From 319563ee0c7a5f8ad4e2609895965d9262a839c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= Date: Mon, 4 Aug 2025 16:59:10 +0200 Subject: [PATCH 1/5] feat(beeai-sdk): implement platform client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- .../src/beeai_sdk/platform/__init__.py | 11 + apps/beeai-sdk/src/beeai_sdk/platform/file.py | 133 ++++++++++++ .../src/beeai_sdk/platform/provider.py | 97 +++++++++ .../src/beeai_sdk/platform/variables.py | 31 +++ .../src/beeai_sdk/platform/vector_store.py | 193 ++++++++++++++++++ 5 files changed, 465 insertions(+) create mode 100644 apps/beeai-sdk/src/beeai_sdk/platform/__init__.py create mode 100644 apps/beeai-sdk/src/beeai_sdk/platform/file.py create mode 100644 apps/beeai-sdk/src/beeai_sdk/platform/provider.py create mode 100644 apps/beeai-sdk/src/beeai_sdk/platform/variables.py create mode 100644 apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py b/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py new file mode 100644 index 000000000..c30c3c33a --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py @@ -0,0 +1,11 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +from functools import cache + +import httpx + + +@cache +def get_client() -> httpx.AsyncClient: + return httpx.AsyncClient(base_url="http://127.0.0.1:8333") diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/file.py b/apps/beeai-sdk/src/beeai_sdk/platform/file.py new file mode 100644 index 000000000..9c4848005 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/platform/file.py @@ -0,0 +1,133 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import typing + +import httpx +import pydantic + +from beeai_sdk.platform import get_client + + +class Extraction(pydantic.BaseModel): + id: str + file_id: str + extracted_file_id: str | None = None + status: typing.Literal["pending", "in_progress", "completed", "failed", "cancelled"] = "pending" + job_id: str | None = None + error_message: str | None = None + extraction_metadata: dict[str, typing.Any] | None = None + started_at: pydantic.AwareDatetime | None = None + finished_at: pydantic.AwareDatetime | None = None + created_at: pydantic.AwareDatetime + + +class File(pydantic.BaseModel): + id: str + filename: str + file_size_bytes: int + created_at: pydantic.AwareDatetime + created_by: str + file_type: typing.Literal["user_upload", "extracted_text"] + parent_file_id: str | None = None + + @staticmethod + async def create( + *, + filename: str, + content: typing.BinaryIO | bytes, + client: httpx.AsyncClient | None = None, + ) -> File: + return pydantic.TypeAdapter(File).validate_python( + ( + await (client or get_client()).post( + url="/api/v1/files", + files={filename: content}, + ) + ) + .raise_for_status() + .json() + ) + + async def get( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> File: + # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `File.get("123")` to obtain a new instance + file_id = self if isinstance(self, str) else self.id + return pydantic.TypeAdapter(File).validate_python( + (await (client or get_client()).get(url=f"/api/v1/files/{file_id}")).raise_for_status().json() + ) + + async def delete( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> None: + # `self` has a weird type so that you can call both `instance.delete()` or `File.delete("123")` + file_id = self if isinstance(self, str) else self.id + _ = (await (client or get_client()).delete(url=f"/api/v1/files/{file_id}")).raise_for_status() + + async def content( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> str: + # `self` has a weird type so that you can call both `instance.content()` to get content of an instance, or `File.content("123")` + file_id = self if isinstance(self, str) else self.id + return (await (client or get_client()).get(url=f"/api/v1/files/{file_id}/content")).raise_for_status().text + + async def text_content( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> str: + # `self` has a weird type so that you can call both `instance.text_content()` to get text content of an instance, or `File.text_content("123")` + file_id = self if isinstance(self, str) else self.id + return (await (client or get_client()).get(url=f"/api/v1/files/{file_id}/text_content")).raise_for_status().text + + async def create_extraction( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> Extraction: + # `self` has a weird type so that you can call both `instance.create_extraction()` to create an extraction for an instance, or `File.create_extraction("123")` + file_id = self if isinstance(self, str) else self.id + return pydantic.TypeAdapter(Extraction).validate_python( + ( + await (client or get_client()).post( + url=f"/api/v1/files/{file_id}/extraction", + ) + ) + .raise_for_status() + .json() + ) + + async def get_extraction( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> Extraction: + # `self` has a weird type so that you can call both `instance.get_extraction()` to get an extraction of an instance, or `File.get_extraction("123", "456")` + file_id = self if isinstance(self, str) else self.id + return pydantic.TypeAdapter(Extraction).validate_python( + ( + await (client or get_client()).get( + url=f"/api/v1/files/{file_id}/extraction", + ) + ) + .raise_for_status() + .json() + ) + + async def delete_extraction( + self: File | str, + *, + client: httpx.AsyncClient | None = None, + ) -> None: + # `self` has a weird type so that you can call both `instance.delete_extraction()` or `File.delete_extraction("123", "456")` + file_id = self if isinstance(self, str) else self.id + _ = (await (client or get_client()).delete(url=f"/api/v1/files/{file_id}/extraction")).raise_for_status() diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/provider.py b/apps/beeai-sdk/src/beeai_sdk/platform/provider.py new file mode 100644 index 000000000..dda613022 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/platform/provider.py @@ -0,0 +1,97 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import typing +from datetime import timedelta + +import httpx +import pydantic +from a2a.types import AgentCard + +from beeai_sdk.platform import get_client + + +class ProviderErrorMessage(pydantic.BaseModel): + message: str + + +class EnvVar(pydantic.BaseModel): + name: str + description: str | None = None + required: bool = False + + +class Provider(pydantic.BaseModel): + id: str + auto_stop_timeout: timedelta + source: str + registry: str | None = None + auto_remove: bool = False + created_at: pydantic.AwareDatetime + last_active_at: pydantic.AwareDatetime + agent_card: AgentCard + state: typing.Literal["missing", "starting", "ready", "running", "error"] = "missing" + last_error: ProviderErrorMessage | None = None + missing_configuration: list[EnvVar] = pydantic.Field(default_factory=list) + + @staticmethod + async def create( + *, + location: str, + agent_card: AgentCard, + auto_remove: bool = False, + client: httpx.AsyncClient | None = None, + ) -> Provider: + return pydantic.TypeAdapter(Provider).validate_python( + ( + await (client or get_client()).post( + url="/api/v1/providers", + json={"location": location, "agent_card": agent_card.model_dump(mode="json")}, + params={"auto_remove": auto_remove}, + ) + ) + .raise_for_status() + .json() + ) + + @staticmethod + async def preview( + *, + location: str, + agent_card: AgentCard, + client: httpx.AsyncClient | None = None, + ) -> Provider: + return pydantic.TypeAdapter(Provider).validate_python( + ( + await (client or get_client()).post( + url="/api/v1/providers/preview", + json={"location": location, "agent_card": agent_card.model_dump(mode="json")}, + ) + ) + .raise_for_status() + .json() + ) + + async def get(self: Provider | str, /, *, client: httpx.AsyncClient | None = None) -> Provider: + # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `Provider.get("123")` to obtain a new instance + provider_id = self if isinstance(self, str) else self.id + result = pydantic.TypeAdapter(Provider).validate_json( + (await (client or get_client()).get(url=f"/api/v1/providers/{provider_id}")).raise_for_status().content + ) + if isinstance(self, Provider): + self.__dict__.update(result.__dict__) + return self + return result + + async def delete(self: Provider | str, /, *, client: httpx.AsyncClient | None = None) -> None: + # `self` has a weird type so that you can call both `instance.delete()` or `Provider.delete("123")` + provider_id = self if isinstance(self, str) else self.id + _ = (await (client or get_client()).delete(f"/api/v1/providers/{provider_id}")).raise_for_status() + + @staticmethod + async def list(*, client: httpx.AsyncClient | None = None) -> list[Provider]: + return pydantic.TypeAdapter(list[Provider]).validate_python( + (await (client or get_client()).get(url="/api/v1/providers")).raise_for_status().json()["items"] + ) diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/variables.py b/apps/beeai-sdk/src/beeai_sdk/platform/variables.py new file mode 100644 index 000000000..fffdb27f6 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/platform/variables.py @@ -0,0 +1,31 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import httpx +import pydantic + +from beeai_sdk.platform import get_client + + +class Variables(pydantic.BaseModel): + env: dict[str, str] = pydantic.Field(default_factory=dict) + + async def save( + self, + *, + client: httpx.AsyncClient | None = None, + ) -> None: + _ = ( + await (client or get_client()).put( + url="/api/v1/variables", + json={"env": self.env}, + ) + ).raise_for_status() + + @staticmethod + async def get(*, client: httpx.AsyncClient | None = None) -> Variables: + return pydantic.TypeAdapter(Variables).validate_json( + (await (client or get_client()).get(url="/api/v1/variables")).raise_for_status().content + ) diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py new file mode 100644 index 000000000..d76e91079 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py @@ -0,0 +1,193 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import typing +from textwrap import dedent +from uuid import UUID, uuid4 + +import httpx +import pydantic + +from beeai_sdk.platform import get_client + + +def validate_metadata(metadata: dict[str, str]) -> dict[str, str]: + if len(metadata) > 16: + raise ValueError("Metadata must be less than 16 keys.") + if any(len(v) > 64 for v in metadata): + raise ValueError("Metadata keys must be less than 64 characters.") + if any(len(v) > 512 for v in metadata.values()): + raise ValueError("Metadata values must be less than 512 characters.") + return metadata + + +Metadata = typing.Annotated[ + dict[str, str], + pydantic.Field( + description=dedent( + """ + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional + information about the object in a structured format, and querying for objects via API or the dashboard. + + Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of + 512 characters. + """, + ) + ), + pydantic.AfterValidator(validate_metadata), +] + + +class VectorStoreStats(pydantic.BaseModel): + usage_bytes: int + num_documents: int + + +class VectorStoreDocumentInfo(pydantic.BaseModel): + id: str + usage_bytes: int | None = None + + +class VectorStoreItem(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=uuid4) + document_id: str + document_type: typing.Literal["platform_file", "external"] = "platform_file" + model_id: str | typing.Literal["platform"] = "platform" + text: str + embedding: list[float] + metadata: Metadata | None = None + + +class VectorStoreSearchResult(pydantic.BaseModel): + item: VectorStoreItem + score: float + + +class VectorStore(pydantic.BaseModel): + id: UUID + name: str | None = None + model_id: str + dimension: int + created_at: pydantic.AwareDatetime + last_active_at: pydantic.AwareDatetime + created_by: UUID + stats: VectorStoreStats | None = None + + @staticmethod + async def create( + *, + name: str, + dimension: int, + model_id: str, + client: httpx.AsyncClient | None = None, + ) -> VectorStore: + return pydantic.TypeAdapter(VectorStore).validate_json( + ( + await (client or get_client()).post( + url="/api/v1/vector_stores", + json={"name": name, "dimension": dimension, "model_id": model_id}, + ) + ) + .raise_for_status() + .content + ) + + async def get( + self: VectorStore | str, + /, + *, + client: httpx.AsyncClient | None = None, + ) -> VectorStore: + # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `VectorStore.get("123")` to obtain a new instance + vector_store_id = self if isinstance(self, str) else self.id + result = pydantic.TypeAdapter(VectorStore).validate_json( + ( + await (client or get_client()).get( + url=f"/api/v1/vector_stores/{vector_store_id}", + ) + ) + .raise_for_status() + .content + ) + if isinstance(self, VectorStore): + self.__dict__.update(result.__dict__) + return self + return result + + async def delete( + self: VectorStore | str, + /, + *, + client: httpx.AsyncClient | None = None, + ) -> None: + # `self` has a weird type so that you can call both `instance.delete()` or `VectorStore.delete("123")` + vector_store_id = self if isinstance(self, str) else self.id + _ = ( + await (client or get_client()).delete( + url=f"/api/v1/vector_stores/{vector_store_id}", + ) + ).raise_for_status() + + async def add_documents( + self: VectorStore | str, /, items: list[VectorStoreItem], *, client: httpx.AsyncClient | None = None + ) -> None: + # `self` has a weird type so that you can call both `instance.add_documents()` or `VectorStore.add_documents("123", items)` + vector_store_id = self if isinstance(self, str) else self.id + _ = ( + await (client or get_client()).put( + url=f"/api/v1/vector_stores/{vector_store_id}", + json=[item.model_dump(mode="json") for item in items], + ) + ).raise_for_status() + + async def search( + self: VectorStore | str, + /, + query_vector: list[float], + *, + limit: int = 10, + client: httpx.AsyncClient | None = None, + ) -> list[VectorStoreSearchResult]: + # `self` has a weird type so that you can call both `instance.search()` to search within an instance, or `VectorStore.search("123", query_vector)` + vector_store_id = self if isinstance(self, str) else self.id + return pydantic.TypeAdapter(list[VectorStoreSearchResult]).validate_python( + ( + await (client or get_client()).post( + url=f"/api/v1/vector_stores/{vector_store_id}/search", + json={"query_vector": query_vector, "limit": limit}, + ) + ) + .raise_for_status() + .json()["items"] + ) + + async def list_documents( + self: VectorStore | str, + /, + *, + client: httpx.AsyncClient | None = None, + ) -> list[VectorStoreItem]: + # `self` has a weird type so that you can call both `instance.list_documents()` to list documents in an instance, or `VectorStore.list_documents("123")` + vector_store_id = self if isinstance(self, str) else self.id + return pydantic.TypeAdapter(list[VectorStoreItem]).validate_python( + (await (client or get_client()).get(url=f"/api/v1/vector_stores/{vector_store_id}/documents")) + .raise_for_status() + .json()["items"] + ) + + async def delete_document( + self: VectorStore | str, + /, + document_id: str, + *, + client: httpx.AsyncClient | None = None, + ) -> None: + # `self` has a weird type so that you can call both `instance.delete_document()` or `VectorStore.delete_document("123", "456")` + vector_store_id = self if isinstance(self, str) else self.id + _ = ( + await (client or get_client()).delete( + url=f"/api/v1/vector_stores/{vector_store_id}/documents/{document_id}", + ) + ).raise_for_status() From 21572c3a300127412353fb8d1f2566caf5832b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= Date: Mon, 4 Aug 2025 21:51:00 +0200 Subject: [PATCH 2/5] test(beeai-sdk): switch beeai-server e2e tests to use beeai-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- apps/beeai-sdk/pyproject.toml | 1 + apps/beeai-sdk/src/beeai_sdk/platform/file.py | 3 +- .../src/beeai_sdk/platform/vector_store.py | 17 +- apps/beeai-sdk/uv.lock | 2 + apps/beeai-server/pyproject.toml | 30 +- apps/beeai-server/tasks.toml | 4 +- apps/beeai-server/tests/e2e/__init__.py | 3 - apps/beeai-server/tests/e2e/conftest.py | 4 +- .../beeai-server/tests/e2e/routes/__init__.py | 3 - .../tests/e2e/routes/a2a/__init__.py | 3 - .../tests/e2e/routes/test_files.py | 107 ++--- .../tests/e2e/routes/test_vector_stores.py | 427 ++++++++---------- .../tests/e2e/test_agent_starts.py | 4 +- apps/beeai-server/uv.lock | 298 ++++++------ 14 files changed, 416 insertions(+), 490 deletions(-) delete mode 100644 apps/beeai-server/tests/e2e/__init__.py delete mode 100644 apps/beeai-server/tests/e2e/routes/__init__.py delete mode 100644 apps/beeai-server/tests/e2e/routes/a2a/__init__.py diff --git a/apps/beeai-sdk/pyproject.toml b/apps/beeai-sdk/pyproject.toml index 7f1ccd9ae..470da53b0 100644 --- a/apps/beeai-sdk/pyproject.toml +++ b/apps/beeai-sdk/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "tenacity>=9.1.2", "janus>=2.0.0", "uvloop>=0.21.0", + "httpx", # version determined by a2a-sdk ] [dependency-groups] diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/file.py b/apps/beeai-sdk/src/beeai_sdk/platform/file.py index 9c4848005..9b0cdb4fb 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/file.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/file.py @@ -38,13 +38,14 @@ async def create( *, filename: str, content: typing.BinaryIO | bytes, + content_type: str = "application/octet-stream", client: httpx.AsyncClient | None = None, ) -> File: return pydantic.TypeAdapter(File).validate_python( ( await (client or get_client()).post( url="/api/v1/files", - files={filename: content}, + files={"file": (filename, content, content_type)}, ) ) .raise_for_status() diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py index d76e91079..178138e97 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py @@ -4,8 +4,8 @@ from __future__ import annotations import typing +import uuid from textwrap import dedent -from uuid import UUID, uuid4 import httpx import pydantic @@ -45,13 +45,16 @@ class VectorStoreStats(pydantic.BaseModel): num_documents: int -class VectorStoreDocumentInfo(pydantic.BaseModel): +class VectorStoreDocument(pydantic.BaseModel): id: str + vector_store_id: str + file_id: str | None = None usage_bytes: int | None = None + created_at: pydantic.AwareDatetime class VectorStoreItem(pydantic.BaseModel): - id: UUID = pydantic.Field(default_factory=uuid4) + id: str = pydantic.Field(default_factory=lambda: uuid.uuid4().hex) document_id: str document_type: typing.Literal["platform_file", "external"] = "platform_file" model_id: str | typing.Literal["platform"] = "platform" @@ -66,13 +69,13 @@ class VectorStoreSearchResult(pydantic.BaseModel): class VectorStore(pydantic.BaseModel): - id: UUID + id: str name: str | None = None model_id: str dimension: int created_at: pydantic.AwareDatetime last_active_at: pydantic.AwareDatetime - created_by: UUID + created_by: str stats: VectorStoreStats | None = None @staticmethod @@ -168,10 +171,10 @@ async def list_documents( /, *, client: httpx.AsyncClient | None = None, - ) -> list[VectorStoreItem]: + ) -> list[VectorStoreDocument]: # `self` has a weird type so that you can call both `instance.list_documents()` to list documents in an instance, or `VectorStore.list_documents("123")` vector_store_id = self if isinstance(self, str) else self.id - return pydantic.TypeAdapter(list[VectorStoreItem]).validate_python( + return pydantic.TypeAdapter(list[VectorStoreDocument]).validate_python( (await (client or get_client()).get(url=f"/api/v1/vector_stores/{vector_store_id}/documents")) .raise_for_status() .json()["items"] diff --git a/apps/beeai-sdk/uv.lock b/apps/beeai-sdk/uv.lock index ee2beb50e..1fac9cdc8 100644 --- a/apps/beeai-sdk/uv.lock +++ b/apps/beeai-sdk/uv.lock @@ -215,6 +215,7 @@ dependencies = [ { name = "a2a-sdk" }, { name = "anyio" }, { name = "asyncclick" }, + { name = "httpx" }, { name = "janus" }, { name = "objprint" }, { name = "opentelemetry-api" }, @@ -243,6 +244,7 @@ requires-dist = [ { name = "a2a-sdk", specifier = ">=0.2.16" }, { name = "anyio", specifier = ">=4.9.0" }, { name = "asyncclick", specifier = ">=8.1.8" }, + { name = "httpx" }, { name = "janus", specifier = ">=2.0.0" }, { name = "objprint", specifier = ">=0.3.0" }, { name = "opentelemetry-api", specifier = ">=1.35.0" }, diff --git a/apps/beeai-server/pyproject.toml b/apps/beeai-server/pyproject.toml index 3b64ca1a1..57da97de9 100644 --- a/apps/beeai-server/pyproject.toml +++ b/apps/beeai-server/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" authors = [{ name = "IBM Corp." }] requires-python = ">=3.13,<4.0" dependencies = [ - "a2a-sdk>=0.2.11", + "a2a-sdk~=0.2.11", "aiohttp>=3.11.11", "anyio>=4.9.0", "asgiref>=3.8.1", @@ -43,6 +43,22 @@ dependencies = [ "openai>=1.97.0", ] +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "pytest-env>=1.1.5", + "pytest-httpx>=0.35.0", + "pytest-subtests>=0.14.1", + "reportlab>=4.4.2", + "pyright>=1.1.399", + "ruff>=0.8.5", + "beeai-sdk", +] + +[tool.uv.sources] +beeai-sdk = { path = "../beeai-sdk", editable = true } + [project.scripts] beeai-server = "beeai_server:serve" migrate = "beeai_server:migrate" @@ -66,18 +82,6 @@ env = [ "AUTH__DISABLE_AUTH=true", ] -[dependency-groups] -dev = [ - "pytest>=8.3.4", - "pytest-asyncio>=0.25.3", - "pytest-env>=1.1.5", - "pytest-httpx>=0.35.0", - "pytest-subtests>=0.14.1", - "reportlab>=4.4.2", - "pyright>=1.1.399", - "ruff>=0.8.5", -] - [tool.ruff] line-length = 120 target-version = "py313" diff --git a/apps/beeai-server/tasks.toml b/apps/beeai-server/tasks.toml index 6e2945e5c..26ce1cbe1 100644 --- a/apps/beeai-server/tasks.toml +++ b/apps/beeai-server/tasks.toml @@ -91,7 +91,7 @@ run = "docker build -t ghcr.io/i-am-bee/beeai-platform/beeai-server:local --load ["beeai-server:build:requirements"] depends = ["beeai-server:setup"] dir = "{{config_root}}/apps/beeai-server" -run = "mkdir -p dist && uv export --no-hashes --no-emit-workspace --format requirements-txt > dist/requirements.txt" +run = "mkdir -p dist && uv export --no-hashes --no-emit-workspace --no-dev --format requirements-txt > dist/requirements.txt" sources = ["uv.lock"] outputs = ["dist/requirements.txt"] @@ -231,12 +231,12 @@ run = "{{ mise_bin }} run beeai-server:dev:reconnect --vm-name=beeai-local-test" dir = "{{config_root}}/apps/beeai-server" run = """ #!/bin/bash +VM_NAME=e2e-test-run {{ mise_bin }} run beeai-platform:stop-all {{ mise_bin }} run beeai-platform:delete --vm-name=${VM_NAME} curl http://localhost:8333 >/dev/null 2>&1 && echo "Another instance at localhost:8333 is already running" && exit 2 -VM_NAME=e2e-test-run {{ mise_bin }} run beeai-platform:start \ --vm-name=${VM_NAME} \ --set externalRegistries=null \ diff --git a/apps/beeai-server/tests/e2e/__init__.py b/apps/beeai-server/tests/e2e/__init__.py deleted file mode 100644 index 32f1f133b..000000000 --- a/apps/beeai-server/tests/e2e/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright 2025 © BeeAI a Series of LF Projects, LLC -# SPDX-License-Identifier: Apache-2.0 - diff --git a/apps/beeai-server/tests/e2e/conftest.py b/apps/beeai-server/tests/e2e/conftest.py index 78ca27dae..1723e448d 100644 --- a/apps/beeai-server/tests/e2e/conftest.py +++ b/apps/beeai-server/tests/e2e/conftest.py @@ -16,9 +16,7 @@ @pytest_asyncio.fixture() async def api_client(test_configuration) -> AsyncIterator[httpx.AsyncClient]: - async with httpx.AsyncClient( - base_url=f"{test_configuration.server_url.rstrip('/')}/api/v1", timeout=None - ) as client: + async with httpx.AsyncClient(base_url=test_configuration.server_url, timeout=None) as client: yield client diff --git a/apps/beeai-server/tests/e2e/routes/__init__.py b/apps/beeai-server/tests/e2e/routes/__init__.py deleted file mode 100644 index 32f1f133b..000000000 --- a/apps/beeai-server/tests/e2e/routes/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright 2025 © BeeAI a Series of LF Projects, LLC -# SPDX-License-Identifier: Apache-2.0 - diff --git a/apps/beeai-server/tests/e2e/routes/a2a/__init__.py b/apps/beeai-server/tests/e2e/routes/a2a/__init__.py deleted file mode 100644 index 32f1f133b..000000000 --- a/apps/beeai-server/tests/e2e/routes/a2a/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright 2025 © BeeAI a Series of LF Projects, LLC -# SPDX-License-Identifier: Apache-2.0 - diff --git a/apps/beeai-server/tests/e2e/routes/test_files.py b/apps/beeai-server/tests/e2e/routes/test_files.py index 04fd0d366..c87cf9dca 100644 --- a/apps/beeai-server/tests/e2e/routes/test_files.py +++ b/apps/beeai-server/tests/e2e/routes/test_files.py @@ -1,11 +1,12 @@ # Copyright 2025 © BeeAI a Series of LF Projects, LLC # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable +from datetime import timedelta from io import BytesIO import httpx import pytest -from asyncpg.pgproto.pgproto import timedelta +from beeai_sdk.platform.file import File from tenacity import AsyncRetrying, stop_after_delay, wait_fixed pytestmark = pytest.mark.e2e @@ -15,29 +16,23 @@ @pytest.mark.usefixtures("clean_up") async def test_files(subtests, setup_real_llm, api_client): with subtests.test("upload file"): - response = await api_client.post( - "files", files={"file": ("test.txt", '{"hello": "world"}', "application/json")} + file = await File.create( + filename="test.txt", content=b'{"hello": "world"}', content_type="application/json", client=api_client ) - response.raise_for_status() - file_id = response.json()["id"] + file_id = file.id with subtests.test("get file metadata"): - response = await api_client.get(f"files/{file_id}") - response.raise_for_status() - assert response.json()["id"] == file_id + retrieved_file = await File.get(file_id, client=api_client) + assert retrieved_file.id == file_id with subtests.test("get file content"): - response = await api_client.get(f"files/{file_id}/content") - response.raise_for_status() - assert response.json() == {"hello": "world"} - assert response.headers["Content-Type"] == "application/json" + content = await retrieved_file.content(client=api_client) + assert content == '{"hello": "world"}' with subtests.test("delete file"): - response = await api_client.delete(f"files/{file_id}") - response.raise_for_status() + await File.delete(file_id, client=api_client) with pytest.raises(httpx.HTTPStatusError, match="404 Not Found"): - response = await api_client.get(f"files/{file_id}") - response.raise_for_status() + await File.get(file_id, client=api_client) @pytest.fixture @@ -68,65 +63,52 @@ async def test_text_extraction_pdf_workflow(subtests, api_client, test_pdf: Call """Test complete PDF text extraction workflow: upload -> extract -> wait -> verify""" # Create a simple PDF-like content for testing - # In a real scenario, you would use a proper PDF file - - file_id = None pdf = test_pdf( "Test of sirens\n" * 100 + "\nBeeai is the future of AI\n\nThere is no better platform than the beeai platform." ) with subtests.test("upload PDF file"): - response = await api_client.post("files", files={"file": ("test_document.pdf", pdf, "application/pdf")}) - response.raise_for_status() - file_data = response.json() - file_id = file_data["id"] - assert file_data["filename"] == "test_document.pdf" - assert file_data["file_type"] == "user_upload" + file = await File.create( + filename="test_document.pdf", content=pdf, content_type="application/pdf", client=api_client + ) + assert file.filename == "test_document.pdf" + assert file.file_type == "user_upload" with subtests.test("create text extraction"): - response = await api_client.post(f"files/{file_id}/extraction") - response.raise_for_status() - extraction_data = response.json() - assert extraction_data["file_id"] == file_id - assert extraction_data["status"] in ["pending", "in_progress", "completed"] + extraction = await file.create_extraction(client=api_client) + assert extraction.file_id == file.id + assert extraction.status in ["pending", "in_progress", "completed"] with subtests.test("check extraction status"): - response = await api_client.get(f"files/{file_id}/extraction") - response.raise_for_status() - extraction_data = response.json() - assert extraction_data["file_id"] == file_id + extraction = await file.get_extraction(client=api_client) + assert extraction.file_id == file.id async for attempt in AsyncRetrying(stop=stop_after_delay(timedelta(seconds=40)), wait=wait_fixed(1)): with attempt: - response = await api_client.get(f"files/{file_id}/extraction") - response.raise_for_status() - extraction_data = response.json() - final_status = extraction_data["status"] + extraction = await file.get_extraction(client=api_client) + final_status = extraction.status if final_status not in ["completed", "failed"]: raise ValueError("not completed") - assert final_status == "completed", ( - f"Expected completed status, got {final_status}: {extraction_data['error_message']}" - ) - assert extraction_data["extracted_file_id"] is not None - assert extraction_data["finished_at"] is not None + assert final_status == "completed", f"Expected completed status, got {final_status}: {extraction.error_message}" + assert extraction.extracted_file_id is not None + assert extraction.finished_at is not None with subtests.test("verify extracted text content"): - response = await api_client.get(f"files/{file_id}/text_content") - response.raise_for_status() + content = await file.text_content(client=api_client) # Check that we get some text content back - content = response.text assert len(content) > 0, "No text content was extracted" assert "Beeai is the future of AI" in content with subtests.test("delete extraction"): - response = await api_client.delete(f"files/{file_id}/extraction") - response.raise_for_status() + await file.delete_extraction(client=api_client) - with subtests.test("verify extraction deleted"), pytest.raises(httpx.HTTPStatusError, match="404 Not Found"): - response = await api_client.get(f"files/{file_id}/extraction") - response.raise_for_status() + with ( + subtests.test("verify extraction deleted"), + pytest.raises(httpx.HTTPStatusError, match="404 Not Found"), + ): + await file.get_extraction(client=api_client) @pytest.mark.asyncio @@ -135,26 +117,19 @@ async def test_text_extraction_plain_text_workflow(subtests, setup_real_llm, api """Test text extraction for plain text files (should be immediate)""" text_content = "This is a sample text document with some content for testing text extraction." - file_id = None with subtests.test("upload text file"): - response = await api_client.post("files", files={"file": ("test_document.txt", text_content, "text/plain")}) - response.raise_for_status() - file_data = response.json() - file_id = file_data["id"] - assert file_data["filename"] == "test_document.txt" + file = await File.create( + filename="test_document.txt", content=text_content.encode(), content_type="text/plain", client=api_client + ) + assert file.filename == "test_document.txt" with subtests.test("create text extraction for plain text"): - response = await api_client.post(f"files/{file_id}/extraction") - response.raise_for_status() - extraction_data = response.json() - assert extraction_data["file_id"] == file_id + extraction = await file.create_extraction(client=api_client) + assert extraction.file_id == file.id # Plain text files should be completed immediately - assert extraction_data["status"] == "completed" + assert extraction.status == "completed" with subtests.test("verify immediate text content access"): - response = await api_client.get(f"files/{file_id}/text_content") - response.raise_for_status() - - extracted_content = response.text + extracted_content = await file.text_content(client=api_client) assert extracted_content == text_content diff --git a/apps/beeai-server/tests/e2e/routes/test_vector_stores.py b/apps/beeai-server/tests/e2e/routes/test_vector_stores.py index 498cf7675..33b288c88 100644 --- a/apps/beeai-server/tests/e2e/routes/test_vector_stores.py +++ b/apps/beeai-server/tests/e2e/routes/test_vector_stores.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import pytest +from beeai_sdk.platform.vector_store import VectorStore, VectorStoreItem pytestmark = pytest.mark.e2e @@ -10,69 +11,62 @@ @pytest.mark.usefixtures("clean_up") async def test_vector_stores(subtests, api_client): items = [ - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "The quick brown fox jumps over the lazy dog.", - "embedding": [1.0] * 127 + [4.0], - "metadata": {"source": "document_a.txt", "chapter": "1"}, - }, - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Artificial intelligence is transforming industries.", - "embedding": [1.0] * 127 + [3.0], - "metadata": {"source": "document_a.txt", "chapter": "2"}, - }, - { - "document_id": "doc_003", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Vector databases are optimized for similarity searches.", - "embedding": [1.0] * 127 + [2.0], - "metadata": None, - }, + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="The quick brown fox jumps over the lazy dog.", + embedding=[1.0] * 127 + [4.0], + metadata={"source": "document_a.txt", "chapter": "1"}, + ), + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="Artificial intelligence is transforming industries.", + embedding=[1.0] * 127 + [3.0], + metadata={"source": "document_a.txt", "chapter": "2"}, + ), + VectorStoreItem( + document_id="doc_003", + document_type="external", + model_id="custom_model_id", + text="Vector databases are optimized for similarity searches.", + embedding=[1.0] * 127 + [2.0], + metadata=None, + ), ] with subtests.test("create a vector store"): - response = await api_client.post( - "vector_stores", json={"name": "test-vector-store", "dimension": 128, "model_id": "custom_model_id"} + vector_store = await VectorStore.create( + name="test-vector-store", + dimension=128, + model_id="custom_model_id", + client=api_client, ) - response.raise_for_status() - vector_store_id = response.json()["id"] with subtests.test("upload vectors"): - response = await api_client.put(f"vector_stores/{vector_store_id}", json=items) - response.raise_for_status() + await vector_store.add_documents(items, client=api_client) with subtests.test("verify usage_bytes updated after upload"): - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - vector_store = response.json() - usage_bytes = vector_store.get("stats", {}).get("usage_bytes", 0) + await vector_store.get(client=api_client) + usage_bytes = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes > 0, "Usage bytes should be greater than 0 after uploading vectors" with subtests.test("search vectors"): - response = await api_client.post( - f"vector_stores/{vector_store_id}/search", - json={"query_vector": [1.0] * 127 + [1.0]}, - ) - response.raise_for_status() - response_json = response.json() + search_results = await vector_store.search(query_vector=[1.0] * 127 + [1.0], client=api_client) - # Check that each item has the new structure with item and score - for result in response_json["items"]: - assert "item" in result - assert "score" in result - assert isinstance(result["score"], int | float) - assert 0.0 <= result["score"] <= 1.0 + # Check that each result has the new structure with item and score + for result in search_results: + assert hasattr(result, "item") + assert hasattr(result, "score") + assert isinstance(result.score, (int, float)) + assert 0.0 <= result.score <= 1.0 # Verify the search results order based on the items in the result - assert response_json["items"][0]["item"]["embedding"] == items[2]["embedding"] - assert response_json["items"][1]["item"]["embedding"] == items[1]["embedding"] - assert response_json["items"][2]["item"]["embedding"] == items[0]["embedding"] + assert search_results[0].item.embedding == items[2].embedding + assert search_results[1].item.embedding == items[1].embedding + assert search_results[2].item.embedding == items[0].embedding @pytest.mark.asyncio @@ -80,39 +74,38 @@ async def test_vector_stores(subtests, api_client): async def test_vector_store_deletion(subtests, api_client): """Test vector store deletion functionality""" items = [ - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Sample text for deletion test.", - "embedding": [1.0] * 128, - "metadata": {"source": "test.txt"}, - } + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="Sample text for deletion test.", + embedding=[1.0] * 128, + metadata={"source": "test.txt"}, + ) ] with subtests.test("create vector store for deletion test"): - response = await api_client.post( - "vector_stores", json={"name": "test-deletion-store", "dimension": 128, "model_id": "custom_model_id"} + vector_store = await VectorStore.create( + name="test-deletion-store", + dimension=128, + model_id="custom_model_id", + client=api_client, ) - response.raise_for_status() - vector_store_id = response.json()["id"] + vector_store_id = vector_store.id with subtests.test("add items to vector store"): - response = await api_client.put(f"vector_stores/{vector_store_id}", json=items) - response.raise_for_status() + await vector_store.add_documents(items, client=api_client) with subtests.test("verify vector store exists before deletion"): - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - assert response.json()["id"] == vector_store_id + await vector_store.get(client=api_client) + assert vector_store.id == vector_store_id with subtests.test("delete vector store"): - response = await api_client.delete(f"vector_stores/{vector_store_id}") - response.raise_for_status() + await vector_store.delete(client=api_client) with subtests.test("verify vector store is deleted"): - response = await api_client.get(f"vector_stores/{vector_store_id}") - assert response.status_code == 404 + with pytest.raises(Exception): # Should raise an exception for 404 + await VectorStore.get(str(vector_store_id), client=api_client) @pytest.mark.asyncio @@ -120,79 +113,64 @@ async def test_vector_store_deletion(subtests, api_client): async def test_document_deletion(subtests, api_client): """Test individual document deletion functionality""" initial_items = [ - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "First document text.", - "embedding": [1.0] * 127 + [1.0], - "metadata": {"source": "doc1.txt"}, - }, - { - "document_id": "doc_002", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Second document text.", - "embedding": [1.0] * 127 + [2.0], - "metadata": {"source": "doc2.txt"}, - }, - { - "document_id": "doc_003", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Third document text.", - "embedding": [1.0] * 127 + [3.0], - "metadata": {"source": "doc3.txt"}, - }, + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="First document text.", + embedding=[1.0] * 127 + [1.0], + metadata={"source": "doc1.txt"}, + ), + VectorStoreItem( + document_id="doc_002", + document_type="external", + model_id="custom_model_id", + text="Second document text.", + embedding=[1.0] * 127 + [2.0], + metadata={"source": "doc2.txt"}, + ), + VectorStoreItem( + document_id="doc_003", + document_type="external", + model_id="custom_model_id", + text="Third document text.", + embedding=[1.0] * 127 + [3.0], + metadata={"source": "doc3.txt"}, + ), ] with subtests.test("create vector store"): - response = await api_client.post( - "vector_stores", json={"name": "test-doc-deletion", "dimension": 128, "model_id": "custom_model_id"} + vector_store = await VectorStore.create( + name="test-doc-deletion", + dimension=128, + model_id="custom_model_id", + client=api_client, ) - response.raise_for_status() - vector_store_id = response.json()["id"] with subtests.test("add initial documents"): - response = await api_client.put(f"vector_stores/{vector_store_id}", json=initial_items) - response.raise_for_status() + await vector_store.add_documents(initial_items, client=api_client) with subtests.test("verify all documents exist via search and track usage_bytes"): - response = await api_client.post( - f"vector_stores/{vector_store_id}/search", - json={"query_vector": [1.0] * 128, "limit": 10}, - ) - response.raise_for_status() - search_results = response.json() - assert len(search_results["items"]) == 3 - - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - vector_store = response.json() - usage_bytes_before_deletion = vector_store.get("stats", {}).get("usage_bytes", 0) + search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + assert len(search_results) == 3 + + await vector_store.get(client=api_client) + usage_bytes_before_deletion = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_before_deletion > 0, "Usage bytes should be greater than 0 after adding documents" with subtests.test("delete specific document"): - response = await api_client.delete(f"vector_stores/{vector_store_id}/documents/doc_002") - response.raise_for_status() + await vector_store.delete_document("doc_002", client=api_client) with subtests.test("verify document was deleted and usage_bytes decreased"): - response = await api_client.post( - f"vector_stores/{vector_store_id}/search", - json={"query_vector": [1.0] * 128, "limit": 10}, - ) - response.raise_for_status() - search_results = response.json() - assert len(search_results["items"]) == 2 - document_ids = [item["item"]["document_id"] for item in search_results["items"]] + search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + assert len(search_results) == 2 + document_ids = [result.item.document_id for result in search_results] assert "doc_002" not in document_ids assert "doc_001" in document_ids assert "doc_003" in document_ids - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - vector_store = response.json() - usage_bytes_after_deletion = vector_store.get("stats", {}).get("usage_bytes", 0) + await vector_store.get(client=api_client) + usage_bytes_after_deletion = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_after_deletion < usage_bytes_before_deletion, ( "Usage bytes should decrease after deleting a document" ) @@ -203,107 +181,90 @@ async def test_document_deletion(subtests, api_client): async def test_adding_items_to_existing_documents(subtests, api_client): """Test adding new items to existing documents in vector store""" initial_items = [ - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Initial content for document 1.", - "embedding": [1.0] * 127 + [1.0], - "metadata": {"source": "doc1.txt", "chapter": "1"}, - }, - { - "document_id": "doc_002", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Initial content for document 2.", - "embedding": [1.0] * 127 + [2.0], - "metadata": {"source": "doc2.txt", "chapter": "1"}, - }, + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="Initial content for document 1.", + embedding=[1.0] * 127 + [1.0], + metadata={"source": "doc1.txt", "chapter": "1"}, + ), + VectorStoreItem( + document_id="doc_002", + document_type="external", + model_id="custom_model_id", + text="Initial content for document 2.", + embedding=[1.0] * 127 + [2.0], + metadata={"source": "doc2.txt", "chapter": "1"}, + ), ] additional_items = [ - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Additional content for document 1.", - "embedding": [1.0] * 127 + [1.5], - "metadata": {"source": "doc1.txt", "chapter": "2"}, - }, - { - "document_id": "doc_003", - "document_type": "external", - "model_id": "custom_model_id", - "text": "New document 3 content.", - "embedding": [1.0] * 127 + [3.0], - "metadata": {"source": "doc3.txt", "chapter": "1"}, - }, + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="Additional content for document 1.", + embedding=[1.0] * 127 + [1.5], + metadata={"source": "doc1.txt", "chapter": "2"}, + ), + VectorStoreItem( + document_id="doc_003", + document_type="external", + model_id="custom_model_id", + text="New document 3 content.", + embedding=[1.0] * 127 + [3.0], + metadata={"source": "doc3.txt", "chapter": "1"}, + ), ] with subtests.test("create vector store"): - response = await api_client.post( - "vector_stores", json={"name": "test-add-items", "dimension": 128, "model_id": "custom_model_id"} + vector_store = await VectorStore.create( + name="test-add-items", + dimension=128, + model_id="custom_model_id", + client=api_client, ) - response.raise_for_status() - vector_store_id = response.json()["id"] with subtests.test("verify initial vector store usage_bytes is 0"): - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - vector_store = response.json() - initial_usage_bytes = vector_store.get("stats", {}).get("usage_bytes", 0) + await vector_store.get(client=api_client) + initial_usage_bytes = vector_store.stats.usage_bytes if vector_store.stats else 0 assert initial_usage_bytes == 0 with subtests.test("add initial items"): - response = await api_client.put(f"vector_stores/{vector_store_id}", json=initial_items) - response.raise_for_status() + await vector_store.add_documents(initial_items, client=api_client) with subtests.test("verify initial items count and usage_bytes updated"): - response = await api_client.post( - f"vector_stores/{vector_store_id}/search", - json={"query_vector": [1.0] * 128, "limit": 10}, - ) - response.raise_for_status() - search_results = response.json() - assert len(search_results["items"]) == 2 - - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - vector_store = response.json() - usage_bytes_after_initial = vector_store.get("stats", {}).get("usage_bytes", 0) + search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + assert len(search_results) == 2 + + await vector_store.get(client=api_client) + usage_bytes_after_initial = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_after_initial > 0, "Usage bytes should be greater than 0 after adding items" with subtests.test("add additional items to existing and new documents"): - response = await api_client.put(f"vector_stores/{vector_store_id}", json=additional_items) - response.raise_for_status() + await vector_store.add_documents(additional_items, client=api_client) with subtests.test("verify all items are present and usage_bytes increased"): - response = await api_client.post( - f"vector_stores/{vector_store_id}/search", - json={"query_vector": [1.0] * 128, "limit": 10}, - ) - response.raise_for_status() - search_results = response.json() - assert len(search_results["items"]) == 4 - - response = await api_client.get(f"vector_stores/{vector_store_id}") - response.raise_for_status() - vector_store = response.json() - usage_bytes_after_additional = vector_store.get("stats", {}).get("usage_bytes", 0) + search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + assert len(search_results) == 4 + + await vector_store.get(client=api_client) + usage_bytes_after_additional = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_after_additional > usage_bytes_after_initial, ( "Usage bytes should increase after adding more items" ) with subtests.test("verify document 1 has multiple items"): - doc_001_items = [item for item in search_results["items"] if item["item"]["document_id"] == "doc_001"] + doc_001_items = [result for result in search_results if result.item.document_id == "doc_001"] assert len(doc_001_items) == 2 - chapters = {item["item"]["metadata"]["chapter"] for item in doc_001_items} + chapters = {result.item.metadata["chapter"] for result in doc_001_items if result.item.metadata} assert chapters == {"1", "2"} with subtests.test("verify new document was created"): - doc_003_items = [item for item in search_results["items"] if item["item"]["document_id"] == "doc_003"] + doc_003_items = [result for result in search_results if result.item.document_id == "doc_003"] assert len(doc_003_items) == 1 - assert doc_003_items[0]["item"]["text"] == "New document 3 content." + assert doc_003_items[0].item.text == "New document 3 content." @pytest.mark.asyncio @@ -311,50 +272,42 @@ async def test_adding_items_to_existing_documents(subtests, api_client): async def test_document_listing(subtests, api_client): """Test listing documents in a vector store""" items = [ - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Content for document 1.", - "embedding": [1.0] * 128, - "metadata": {"source": "doc1.txt"}, - }, - { - "document_id": "doc_001", - "document_type": "external", - "model_id": "custom_model_id", - "text": "More content for document 1.", - "embedding": [2.0] * 128, - "metadata": {"source": "doc1.txt"}, - }, - { - "document_id": "doc_002", - "document_type": "external", - "model_id": "custom_model_id", - "text": "Content for document 2.", - "embedding": [3.0] * 128, - "metadata": {"source": "doc2.txt"}, - }, + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="Content for document 1.", + embedding=[1.0] * 128, + metadata={"source": "doc1.txt"}, + ), + VectorStoreItem( + document_id="doc_001", + document_type="external", + model_id="custom_model_id", + text="More content for document 1.", + embedding=[2.0] * 128, + metadata={"source": "doc1.txt"}, + ), + VectorStoreItem( + document_id="doc_002", + document_type="external", + model_id="custom_model_id", + text="Content for document 2.", + embedding=[3.0] * 128, + metadata={"source": "doc2.txt"}, + ), ] with subtests.test("create vector store"): - response = await api_client.post( - "vector_stores", json={"name": "test-doc-listing", "dimension": 128, "model_id": "custom_model_id"} + vector_store = await VectorStore.create( + name="test-doc-listing", + dimension=128, + model_id="custom_model_id", + client=api_client, ) - response.raise_for_status() - vector_store_id = response.json()["id"] with subtests.test("add items to vector store"): - response = await api_client.put(f"vector_stores/{vector_store_id}", json=items) - response.raise_for_status() + await vector_store.add_documents(items, client=api_client) with subtests.test("list documents in vector store"): - response = await api_client.get(f"vector_stores/{vector_store_id}/documents") - response.raise_for_status() - documents = response.json()["items"] - - # Should have 2 unique documents (doc_001 and doc_002) - document_ids = {doc["id"] for doc in documents} - assert len(document_ids) == 2 - assert "doc_001" in document_ids - assert "doc_002" in document_ids + assert {doc.id for doc in await vector_store.list_documents(client=api_client)} == {"doc_001", "doc_002"} diff --git a/apps/beeai-server/tests/e2e/test_agent_starts.py b/apps/beeai-server/tests/e2e/test_agent_starts.py index 846557fb2..c8e226867 100644 --- a/apps/beeai-server/tests/e2e/test_agent_starts.py +++ b/apps/beeai-server/tests/e2e/test_agent_starts.py @@ -22,9 +22,9 @@ async def test_agent(subtests, setup_real_llm, api_client, a2a_client_factory): agent_image = "ghcr.io/i-am-bee/beeai-platform-agent-starter/my-agent-a2a:latest" with subtests.test("add chat agent"): - response = await api_client.post("providers", json={"location": agent_image}) + response = await api_client.post("api/v1/providers", json={"location": agent_image}) response.raise_for_status() - providers_response = await api_client.get("providers") + providers_response = await api_client.get("api/v1/providers") providers_response.raise_for_status() providers = providers_response.json() assert len(providers["items"]) == 1 diff --git a/apps/beeai-server/uv.lock b/apps/beeai-server/uv.lock index 21367d311..d4f6bbea9 100644 --- a/apps/beeai-server/uv.lock +++ b/apps/beeai-server/uv.lock @@ -4,26 +4,21 @@ requires-python = ">=3.13, <4.0" [[package]] name = "a2a-sdk" -version = "0.2.11" +version = "0.2.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, - { name = "google-api-core" }, - { name = "grpcio" }, - { name = "grpcio-reflection" }, - { name = "grpcio-tools" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, - { name = "protobuf" }, { name = "pydantic" }, { name = "sse-starlette" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/cf/d33a415d713d92b5ac15585142ce4e230c8d51018a650bf7a6deb8fb8bd4/a2a_sdk-0.2.11.tar.gz", hash = "sha256:8c267112165471e66e43e855c4ff16fbbfa5d84e408dabf2bd522f177013ae42", size = 164516, upload-time = "2025-07-08T03:07:46.228Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/3b/8fd1e3fe28606712c203b968a6fe2c8e7944b6df9e65c28976c66c19286c/a2a_sdk-0.2.16.tar.gz", hash = "sha256:d9638c71674183f32fe12f8865015e91a563a90a3aa9ed43020f1b23164862b3", size = 179006, upload-time = "2025-07-21T19:51:14.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/72/45d603be8f8853beb5fec5039f74deb64f64359d70e267e728c19231b0a6/a2a_sdk-0.2.11-py3-none-any.whl", hash = "sha256:f3df6e3da8a70c04074efb22863b730061996de10018f83143063f65d9fe0c04", size = 95324, upload-time = "2025-07-08T03:07:44.64Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/16bfbc2ef0ef037c5860ef3b13e482aeb1860b9643bf833ed522c995f639/a2a_sdk-0.2.16-py3-none-any.whl", hash = "sha256:54782eab3d0ad0d5842bfa07ff78d338ea836f1259ece51a825c53193c67c7d0", size = 103090, upload-time = "2025-07-21T19:51:12.613Z" }, ] [[package]] @@ -204,6 +199,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/94/51927deb4f40872361ec4f5534f68f7a9ce81c4ef20bf5cd765307f4c15d/asyncache-0.3.1-py3-none-any.whl", hash = "sha256:ef20a1024d265090dd1e0785c961cf98b9c32cc7d9478973dcf25ac1b80011f5", size = 3722, upload-time = "2022-11-15T10:06:45.546Z" }, ] +[[package]] +name = "asyncclick" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/e1e5fdf1c1bb7e6e614987c120a98d9324bf8edfaa5f5cd16a6235c9d91b/asyncclick-8.1.8.tar.gz", hash = "sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c", size = 232900, upload-time = "2025-01-06T09:46:52.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/cc/a436f0fc2d04e57a0697e0f87a03b9eaed03ad043d2d5f887f8eebcec95f/asyncclick-8.1.8-py3-none-any.whl", hash = "sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6", size = 99093, upload-time = "2025-01-06T09:46:51.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/ae9e9d25522c6dc96ff167903880a0fe94d7bd31ed999198ee5017d977ed/asyncclick-8.1.8.0-py3-none-any.whl", hash = "sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678", size = 99115, upload-time = "2025-01-06T09:50:52.72Z" }, +] + [[package]] name = "asyncpg" version = "0.30.0" @@ -229,6 +238,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "beeai-sdk" +version = "0.3.0rc6" +source = { editable = "../beeai-sdk" } +dependencies = [ + { name = "a2a-sdk" }, + { name = "anyio" }, + { name = "asyncclick" }, + { name = "httpx" }, + { name = "janus" }, + { name = "objprint" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-sdk" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "tenacity" }, + { name = "uvicorn" }, + { name = "uvloop" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", specifier = ">=0.2.16" }, + { name = "anyio", specifier = ">=4.9.0" }, + { name = "asyncclick", specifier = ">=8.1.8" }, + { name = "httpx" }, + { name = "janus", specifier = ">=2.0.0" }, + { name = "objprint", specifier = ">=0.3.0" }, + { name = "opentelemetry-api", specifier = ">=1.35.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.35.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.56b0" }, + { name = "opentelemetry-sdk", specifier = ">=1.35.0" }, + { name = "sse-starlette", specifier = ">=2.2.1" }, + { name = "starlette", specifier = ">=0.47.2" }, + { name = "tenacity", specifier = ">=9.1.2" }, + { name = "uvicorn", specifier = ">=0.35.0" }, + { name = "uvloop", specifier = ">=0.21.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "beeai-framework", extras = ["duckduckgo", "wikipedia"], specifier = ">=0.1.32" }, + { name = "pyright", specifier = ">=1.1.403" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-asyncio", specifier = ">=1.1.0" }, + { name = "pytest-httpx", specifier = ">=0.35.0" }, + { name = "ruff", specifier = ">=0.12.3" }, +] + [[package]] name = "beeai-server" version = "0.3.0rc6" @@ -269,6 +329,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "beeai-sdk" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -281,7 +342,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "a2a-sdk", specifier = ">=0.2.11" }, + { name = "a2a-sdk", specifier = "~=0.2.11" }, { name = "aioboto3", specifier = ">=14.3.0" }, { name = "aiodocker", specifier = ">=0.24.0" }, { name = "aiohttp", specifier = ">=3.11.11" }, @@ -316,6 +377,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "beeai-sdk", editable = "../beeai-sdk" }, { name = "pyright", specifier = ">=1.1.399" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-asyncio", specifier = ">=0.25.3" }, @@ -621,36 +683,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] -[[package]] -name = "google-api-core" -version = "2.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" }, -] - -[[package]] -name = "google-auth" -version = "2.40.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.70.0" @@ -687,60 +719,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, ] -[[package]] -name = "grpcio" -version = "1.73.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/e8/b43b851537da2e2f03fa8be1aef207e5cbfb1a2e014fbb6b40d24c177cd3/grpcio-1.73.1.tar.gz", hash = "sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87", size = 12730355, upload-time = "2025-06-26T01:53:24.622Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/bf/4ca20d1acbefabcaba633ab17f4244cbbe8eca877df01517207bd6655914/grpcio-1.73.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9", size = 5335615, upload-time = "2025-06-26T01:52:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/75/ed/45c345f284abec5d4f6d77cbca9c52c39b554397eb7de7d2fcf440bcd049/grpcio-1.73.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5", size = 10595497, upload-time = "2025-06-26T01:52:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/a4/75/bff2c2728018f546d812b755455014bc718f8cdcbf5c84f1f6e5494443a8/grpcio-1.73.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b", size = 5765321, upload-time = "2025-06-26T01:52:46.871Z" }, - { url = "https://files.pythonhosted.org/packages/70/3b/14e43158d3b81a38251b1d231dfb45a9b492d872102a919fbf7ba4ac20cd/grpcio-1.73.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182", size = 6415436, upload-time = "2025-06-26T01:52:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3f/81d9650ca40b54338336fd360f36773be8cb6c07c036e751d8996eb96598/grpcio-1.73.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854", size = 6007012, upload-time = "2025-06-26T01:52:51.076Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/59edf5af68d684d0f4f7ad9462a418ac517201c238551529098c9aa28cb0/grpcio-1.73.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2", size = 6105209, upload-time = "2025-06-26T01:52:52.773Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a8/700d034d5d0786a5ba14bfa9ce974ed4c976936c2748c2bd87aa50f69b36/grpcio-1.73.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5", size = 6753655, upload-time = "2025-06-26T01:52:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/1f/29/efbd4ac837c23bc48e34bbaf32bd429f0dc9ad7f80721cdb4622144c118c/grpcio-1.73.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668", size = 6287288, upload-time = "2025-06-26T01:52:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/d8/61/c6045d2ce16624bbe18b5d169c1a5ce4d6c3a47bc9d0e5c4fa6a50ed1239/grpcio-1.73.1-cp313-cp313-win32.whl", hash = "sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4", size = 3668151, upload-time = "2025-06-26T01:52:59.405Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/77ac689216daee10de318db5aa1b88d159432dc76a130948a56b3aa671a2/grpcio-1.73.1-cp313-cp313-win_amd64.whl", hash = "sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f", size = 4335747, upload-time = "2025-06-26T01:53:01.233Z" }, -] - -[[package]] -name = "grpcio-reflection" -version = "1.71.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/14/4e5f8e902fa9461abae292773b921a578f68333c7c3e731bcff7514f78cd/grpcio_reflection-1.71.2.tar.gz", hash = "sha256:bedfac3d2095d6c066b16b66bfce85b4be3e92dc9f3b7121e6f019d24a9c09c0", size = 18798, upload-time = "2025-06-28T04:24:06.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/89/c99ff79b90315cf47dbcdd86babb637764e5f14f523d622020bfee57dc4d/grpcio_reflection-1.71.2-py3-none-any.whl", hash = "sha256:c4f1a0959acb94ec9e1369bb7dab827cc9a6efcc448bdb10436246c8e52e2f57", size = 22684, upload-time = "2025-06-28T04:23:44.759Z" }, -] - -[[package]] -name = "grpcio-tools" -version = "1.71.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/9a/edfefb47f11ef6b0f39eea4d8f022c5bb05ac1d14fcc7058e84a51305b73/grpcio_tools-1.71.2.tar.gz", hash = "sha256:b5304d65c7569b21270b568e404a5a843cf027c66552a6a0978b23f137679c09", size = 5330655, upload-time = "2025-06-28T04:22:00.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/9c/bdf9c5055a1ad0a09123402d73ecad3629f75b9cf97828d547173b328891/grpcio_tools-1.71.2-cp313-cp313-linux_armv7l.whl", hash = "sha256:b0f0a8611614949c906e25c225e3360551b488d10a366c96d89856bcef09f729", size = 2384758, upload-time = "2025-06-28T04:21:26.712Z" }, - { url = "https://files.pythonhosted.org/packages/49/d0/6aaee4940a8fb8269c13719f56d69c8d39569bee272924086aef81616d4a/grpcio_tools-1.71.2-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:7931783ea7ac42ac57f94c5047d00a504f72fbd96118bf7df911bb0e0435fc0f", size = 5443127, upload-time = "2025-06-28T04:21:28.383Z" }, - { url = "https://files.pythonhosted.org/packages/d9/11/50a471dcf301b89c0ed5ab92c533baced5bd8f796abfd133bbfadf6b60e5/grpcio_tools-1.71.2-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:d188dc28e069aa96bb48cb11b1338e47ebdf2e2306afa58a8162cc210172d7a8", size = 2349627, upload-time = "2025-06-28T04:21:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/bb/66/e3dc58362a9c4c2fbe98a7ceb7e252385777ebb2bbc7f42d5ab138d07ace/grpcio_tools-1.71.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f36c4b3cc42ad6ef67430639174aaf4a862d236c03c4552c4521501422bfaa26", size = 2742932, upload-time = "2025-06-28T04:21:32.325Z" }, - { url = "https://files.pythonhosted.org/packages/b7/1e/1e07a07ed8651a2aa9f56095411198385a04a628beba796f36d98a5a03ec/grpcio_tools-1.71.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bd9ed12ce93b310f0cef304176049d0bc3b9f825e9c8c6a23e35867fed6affd", size = 2473627, upload-time = "2025-06-28T04:21:33.752Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f9/3b7b32e4acb419f3a0b4d381bc114fe6cd48e3b778e81273fc9e4748caad/grpcio_tools-1.71.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7ce27e76dd61011182d39abca38bae55d8a277e9b7fe30f6d5466255baccb579", size = 2850879, upload-time = "2025-06-28T04:21:35.241Z" }, - { url = "https://files.pythonhosted.org/packages/1e/99/cd9e1acd84315ce05ad1fcdfabf73b7df43807cf00c3b781db372d92b899/grpcio_tools-1.71.2-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:dcc17bf59b85c3676818f2219deacac0156492f32ca165e048427d2d3e6e1157", size = 3300216, upload-time = "2025-06-28T04:21:36.826Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c0/66eab57b14550c5b22404dbf60635c9e33efa003bd747211981a9859b94b/grpcio_tools-1.71.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:706360c71bdd722682927a1fb517c276ccb816f1e30cb71f33553e5817dc4031", size = 2913521, upload-time = "2025-06-28T04:21:38.347Z" }, - { url = "https://files.pythonhosted.org/packages/05/9b/7c90af8f937d77005625d705ab1160bc42a7e7b021ee5c788192763bccd6/grpcio_tools-1.71.2-cp313-cp313-win32.whl", hash = "sha256:bcf751d5a81c918c26adb2d6abcef71035c77d6eb9dd16afaf176ee096e22c1d", size = 945322, upload-time = "2025-06-28T04:21:39.864Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/6db6247f767c94fe551761772f89ceea355ff295fd4574cb8efc8b2d1199/grpcio_tools-1.71.2-cp313-cp313-win_amd64.whl", hash = "sha256:b1581a1133552aba96a730178bc44f6f1a071f0eb81c5b6bc4c0f89f5314e2b8", size = 1117234, upload-time = "2025-06-28T04:21:41.893Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -900,6 +878,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "janus" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/7f/69884b6618be4baf6ebcacc716ee8680a842428a19f403db6d1c0bb990aa/janus-2.0.0.tar.gz", hash = "sha256:0970f38e0e725400496c834a368a67ee551dc3b5ad0a257e132f5b46f2e77770", size = 22910, upload-time = "2024-12-13T12:59:08.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/34/65604740edcb20e1bda6a890348ed7d282e7dd23aa00401cbe36fd0edbd9/janus-2.0.0-py3-none-any.whl", hash = "sha256:7e6449d34eab04cd016befbd7d8c0d8acaaaab67cb59e076a69149f9031745f9", size = 12161, upload-time = "2024-12-13T12:59:06.106Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1146,6 +1133,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" }, ] +[[package]] +name = "objprint" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/b8/c10e96120f1585824a1992655334b49da3924edfb364e84a26cc0ecdb89b/objprint-0.3.0.tar.gz", hash = "sha256:b5d83f9d62db5b95353bb42959106e1cd43010dcaa3eed1ad8d7d0b2df9b2d5a", size = 47481, upload-time = "2024-11-09T00:05:16.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/af/572825252f16f36eeecbc8e3b721913d2640d69b984fdb8907aa8b4b0975/objprint-0.3.0-py3-none-any.whl", hash = "sha256:489083bfc8baf0526f8fd6af74673799511532636f0ce4141133255ded773405", size = 41619, upload-time = "2024-11-09T00:05:14.852Z" }, +] + [[package]] name = "openai" version = "1.97.1" @@ -1208,6 +1204,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/71/f118cd90dc26797077931dd598bde5e0cc652519db166593f962f8fcd022/opentelemetry_exporter_otlp_proto_http-1.35.0-py3-none-any.whl", hash = "sha256:9a001e3df3c7f160fb31056a28ed7faa2de7df68877ae909516102ae36a54e1d", size = 18589, upload-time = "2025-07-11T12:23:13.906Z" }, ] +[[package]] +name = "opentelemetry-instrumentation" +version = "0.56b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/14/964e90f524655aed5c699190dad8dd9a05ed0f5fa334b4b33532237c2b51/opentelemetry_instrumentation-0.56b0.tar.gz", hash = "sha256:d2dbb3021188ca0ec8c5606349ee9a2919239627e8341d4d37f1d21ec3291d11", size = 28551, upload-time = "2025-07-11T12:26:19.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/aa/2328f27200b8e51640d4d7ff5343ba6a81ab7d2650a9f574db016aae4adf/opentelemetry_instrumentation-0.56b0-py3-none-any.whl", hash = "sha256:948967f7c8f5bdc6e43512ba74c9ae14acb48eb72a35b61afe8db9909f743be3", size = 31105, upload-time = "2025-07-11T12:25:22.788Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.56b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/fa/1a1f6a98e8d5a4e9f4f5b7da1543fee4edcaa78eacf24850875823773a08/opentelemetry_instrumentation_asgi-0.56b0.tar.gz", hash = "sha256:e9142c7a5ad81c019070640ab8a1c217d2ca7cb7621e413cde78d0caece8cda8", size = 24654, upload-time = "2025-07-11T12:26:22.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/dd/655355453ee918455b5e7132f80366f594e0e7433dbcd03d2b0d6033088f/opentelemetry_instrumentation_asgi-0.56b0-py3-none-any.whl", hash = "sha256:7144793c1c601e47db6174d748ab437f7fa92f93a87d13882ad60a1f8147d2cf", size = 16602, upload-time = "2025-07-11T12:25:28.7Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.56b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/d9/39d3ea22bb1713cda5f47cb84a7f3ae78ac0d915fcda2d3db1b0b30fa53c/opentelemetry_instrumentation_fastapi-0.56b0.tar.gz", hash = "sha256:83a3949ff6f48177758692265b24bab16830945841aec519a2c012351589c7ce", size = 20275, upload-time = "2025-07-11T12:26:35.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/ab/c0fa41efd8cc1be619c01d04a4ab2dcc8f940c7be0c7cfc98a99de90c085/opentelemetry_instrumentation_fastapi-0.56b0-py3-none-any.whl", hash = "sha256:8d53a17fd329ca3ff7ba98595422007f432d3193f1a8e17cc8bfcd9845213b6d", size = 12711, upload-time = "2025-07-11T12:25:43.176Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.35.0" @@ -1247,6 +1290,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/3f/e80c1b017066a9d999efffe88d1cce66116dcf5cb7f80c41040a83b6e03b/opentelemetry_semantic_conventions-0.56b0-py3-none-any.whl", hash = "sha256:df44492868fd6b482511cc43a942e7194be64e94945f572db24df2e279a001a2", size = 201625, upload-time = "2025-07-11T12:23:25.63Z" }, ] +[[package]] +name = "opentelemetry-util-http" +version = "0.56b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/c5/80c603e44071d172d4e9c909b13e3d9924b90b08a581eff78a8daf77686e/opentelemetry_util_http-0.56b0.tar.gz", hash = "sha256:9a0c8573a68e3242a2d3e5840476088e63714e6d3e25f67127945ab0c7143074", size = 9404, upload-time = "2025-07-11T12:26:55.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/ca/20763fba2af06e73f0e666e46a32b5cdb9d2d75dcb5fd221f50c818cae43/opentelemetry_util_http-0.56b0-py3-none-any.whl", hash = "sha256:e26dd8c7f71da6806f1e65ac7cde189d389b8f152506146968f59b7a607dc8cf", size = 7645, upload-time = "2025-07-11T12:26:16.106Z" }, +] + [[package]] name = "orjson" version = "3.10.18" @@ -1440,18 +1492,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] -[[package]] -name = "proto-plus" -version = "1.26.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, -] - [[package]] name = "protobuf" version = "5.29.5" @@ -1516,27 +1556,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" }, ] -[[package]] -name = "pyasn1" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - [[package]] name = "pycparser" version = "2.22" @@ -1869,18 +1888,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/9f/0f13511b27c3548372d9679637f1120e690370baf6ed890755eb73d9387b/rignore-0.6.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2252d603550d529362c569b10401ab32536613517e7e1df0e4477fe65498245", size = 974567, upload-time = "2025-07-13T11:57:26.592Z" }, ] -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - [[package]] name = "ruff" version = "0.12.3" @@ -1931,15 +1938,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" }, ] -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -2016,14 +2014,14 @@ wheels = [ [[package]] name = "starlette" -version = "0.47.1" +version = "0.47.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, ] [[package]] From 331e3597bf02f0ef79e4842ef69326553cfb71e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= Date: Tue, 5 Aug 2025 16:09:08 +0200 Subject: [PATCH 3/5] chore: lint fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- .../tests/e2e/routes/test_vector_stores.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/beeai-server/tests/e2e/routes/test_vector_stores.py b/apps/beeai-server/tests/e2e/routes/test_vector_stores.py index 33b288c88..ea156cefa 100644 --- a/apps/beeai-server/tests/e2e/routes/test_vector_stores.py +++ b/apps/beeai-server/tests/e2e/routes/test_vector_stores.py @@ -1,6 +1,7 @@ # Copyright 2025 © BeeAI a Series of LF Projects, LLC # SPDX-License-Identifier: Apache-2.0 +import httpx import pytest from beeai_sdk.platform.vector_store import VectorStore, VectorStoreItem @@ -60,7 +61,7 @@ async def test_vector_stores(subtests, api_client): for result in search_results: assert hasattr(result, "item") assert hasattr(result, "score") - assert isinstance(result.score, (int, float)) + assert isinstance(result.score, int | float) assert 0.0 <= result.score <= 1.0 # Verify the search results order based on the items in the result @@ -103,9 +104,11 @@ async def test_vector_store_deletion(subtests, api_client): with subtests.test("delete vector store"): await vector_store.delete(client=api_client) - with subtests.test("verify vector store is deleted"): - with pytest.raises(Exception): # Should raise an exception for 404 - await VectorStore.get(str(vector_store_id), client=api_client) + with ( + subtests.test("verify vector store is deleted"), + pytest.raises(httpx.HTTPStatusError, match="404 Not Found"), + ): + await VectorStore.get(str(vector_store_id), client=api_client) @pytest.mark.asyncio From 1a061dc9ef69d67c5ebbb7bc915be40ba91b5e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= Date: Wed, 6 Aug 2025 13:12:00 +0200 Subject: [PATCH 4/5] feat: implement context based configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- .../src/beeai_sdk/platform/__init__.py | 13 +- .../src/beeai_sdk/platform/context.py | 17 +++ apps/beeai-sdk/src/beeai_sdk/platform/file.py | 2 +- .../src/beeai_sdk/platform/provider.py | 28 ++-- .../src/beeai_sdk/platform/variables.py | 35 +++-- .../src/beeai_sdk/platform/vector_store.py | 2 +- apps/beeai-sdk/src/beeai_sdk/util/__init__.py | 4 + .../src/beeai_sdk/util/resource_context.py | 45 +++++++ apps/beeai-server/tests/e2e/conftest.py | 30 +++-- .../tests/e2e/routes/test_files.py | 49 +++---- .../tests/e2e/routes/test_vector_stores.py | 123 ++++++++++-------- .../tests/e2e/test_agent_starts.py | 27 ++-- 12 files changed, 236 insertions(+), 139 deletions(-) create mode 100644 apps/beeai-sdk/src/beeai_sdk/platform/context.py create mode 100644 apps/beeai-sdk/src/beeai_sdk/util/__init__.py create mode 100644 apps/beeai-sdk/src/beeai_sdk/util/resource_context.py diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py b/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py index c30c3c33a..fda14a6c8 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/__init__.py @@ -1,11 +1,8 @@ # Copyright 2025 © BeeAI a Series of LF Projects, LLC # SPDX-License-Identifier: Apache-2.0 -from functools import cache - -import httpx - - -@cache -def get_client() -> httpx.AsyncClient: - return httpx.AsyncClient(base_url="http://127.0.0.1:8333") +from .context import * +from .file import * +from .provider import * +from .variables import * +from .vector_store import * diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/context.py b/apps/beeai-sdk/src/beeai_sdk/platform/context.py new file mode 100644 index 000000000..08d207bc5 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/platform/context.py @@ -0,0 +1,17 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +import os + +import httpx + +from beeai_sdk.util import resource_context + +get_client, with_client = resource_context( + httpx.AsyncClient, lambda: httpx.AsyncClient(base_url=os.environ.get("PLATFORM_URL", "http://127.0.0.1:8333")) +) + +__all__ = [ + "get_client", + "with_client", +] diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/file.py b/apps/beeai-sdk/src/beeai_sdk/platform/file.py index 9b0cdb4fb..9b381a13a 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/file.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/file.py @@ -8,7 +8,7 @@ import httpx import pydantic -from beeai_sdk.platform import get_client +from beeai_sdk.platform.context import get_client class Extraction(pydantic.BaseModel): diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/provider.py b/apps/beeai-sdk/src/beeai_sdk/platform/provider.py index dda613022..9da048472 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/provider.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/provider.py @@ -1,8 +1,6 @@ # Copyright 2025 © BeeAI a Series of LF Projects, LLC # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - import typing from datetime import timedelta @@ -10,7 +8,7 @@ import pydantic from a2a.types import AgentCard -from beeai_sdk.platform import get_client +from beeai_sdk.platform.context import get_client class ProviderErrorMessage(pydantic.BaseModel): @@ -40,15 +38,18 @@ class Provider(pydantic.BaseModel): async def create( *, location: str, - agent_card: AgentCard, + agent_card: AgentCard | None = None, auto_remove: bool = False, client: httpx.AsyncClient | None = None, - ) -> Provider: + ) -> "Provider": return pydantic.TypeAdapter(Provider).validate_python( ( await (client or get_client()).post( url="/api/v1/providers", - json={"location": location, "agent_card": agent_card.model_dump(mode="json")}, + json={ + "location": location, + "agent_card": agent_card.model_dump(mode="json") if agent_card else None, + }, params={"auto_remove": auto_remove}, ) ) @@ -60,21 +61,24 @@ async def create( async def preview( *, location: str, - agent_card: AgentCard, + agent_card: AgentCard | None = None, client: httpx.AsyncClient | None = None, - ) -> Provider: + ) -> "Provider": return pydantic.TypeAdapter(Provider).validate_python( ( await (client or get_client()).post( url="/api/v1/providers/preview", - json={"location": location, "agent_card": agent_card.model_dump(mode="json")}, + json={ + "location": location, + "agent_card": agent_card.model_dump(mode="json") if agent_card else None, + }, ) ) .raise_for_status() .json() ) - async def get(self: Provider | str, /, *, client: httpx.AsyncClient | None = None) -> Provider: + async def get(self: "Provider | str", /, *, client: httpx.AsyncClient | None = None) -> "Provider": # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `Provider.get("123")` to obtain a new instance provider_id = self if isinstance(self, str) else self.id result = pydantic.TypeAdapter(Provider).validate_json( @@ -85,13 +89,13 @@ async def get(self: Provider | str, /, *, client: httpx.AsyncClient | None = Non return self return result - async def delete(self: Provider | str, /, *, client: httpx.AsyncClient | None = None) -> None: + async def delete(self: "Provider | str", /, *, client: httpx.AsyncClient | None = None) -> None: # `self` has a weird type so that you can call both `instance.delete()` or `Provider.delete("123")` provider_id = self if isinstance(self, str) else self.id _ = (await (client or get_client()).delete(f"/api/v1/providers/{provider_id}")).raise_for_status() @staticmethod - async def list(*, client: httpx.AsyncClient | None = None) -> list[Provider]: + async def list(*, client: httpx.AsyncClient | None = None) -> list["Provider"]: return pydantic.TypeAdapter(list[Provider]).validate_python( (await (client or get_client()).get(url="/api/v1/providers")).raise_for_status().json()["items"] ) diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/variables.py b/apps/beeai-sdk/src/beeai_sdk/platform/variables.py index fffdb27f6..d682f69b4 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/variables.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/variables.py @@ -4,28 +4,41 @@ from __future__ import annotations import httpx -import pydantic -from beeai_sdk.platform import get_client +from beeai_sdk.platform.context import get_client -class Variables(pydantic.BaseModel): - env: dict[str, str] = pydantic.Field(default_factory=dict) - +class Variables(dict[str, str]): async def save( - self, + self: Variables | dict[str, str], *, client: httpx.AsyncClient | None = None, ) -> None: + """ + Save variables to the BeeAI platform. Does not delete keys unless explicitly set to None. + + Can be used as a class method: Variables.save({"key": "value", ...}) + ...or as an instance method: variables.save() + """ _ = ( await (client or get_client()).put( url="/api/v1/variables", - json={"env": self.env}, + json={"env": self}, ) ).raise_for_status() - @staticmethod - async def get(*, client: httpx.AsyncClient | None = None) -> Variables: - return pydantic.TypeAdapter(Variables).validate_json( - (await (client or get_client()).get(url="/api/v1/variables")).raise_for_status().content + async def load(self: Variables | None = None, *, client: httpx.AsyncClient | None = None) -> Variables: + """ + Load variables from the BeeAI platform. + + Can be used as a class method: variables = Variables.load() + ...or as an instance method to update the instance: variables.load() + """ + new_variables: dict[str, str] = ( + (await (client or get_client()).get(url="/api/v1/variables")).raise_for_status().json() ) + if isinstance(self, Variables): + self.clear() + self.update(new_variables) + return self + return Variables(new_variables) diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py index 178138e97..1c0e973d4 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py @@ -10,7 +10,7 @@ import httpx import pydantic -from beeai_sdk.platform import get_client +from beeai_sdk.platform.context import get_client def validate_metadata(metadata: dict[str, str]) -> dict[str, str]: diff --git a/apps/beeai-sdk/src/beeai_sdk/util/__init__.py b/apps/beeai-sdk/src/beeai_sdk/util/__init__.py new file mode 100644 index 000000000..fe5434ca4 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/util/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +from .resource_context import * diff --git a/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py b/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py new file mode 100644 index 000000000..0114d97c5 --- /dev/null +++ b/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py @@ -0,0 +1,45 @@ +# Copyright 2025 © BeeAI a Series of LF Projects, LLC +# SPDX-License-Identifier: Apache-2.0 + +import contextlib +import contextvars +import typing + + +async def noop(): + pass + + +P = typing.ParamSpec("P") +T = typing.TypeVar("T") + + +def resource_context( + resource_factory: typing.Callable[P, T], + default_resource_factory: typing.Callable[[], T], +) -> tuple[typing.Callable[[], T], typing.Callable[P, contextlib.AbstractAsyncContextManager[T]]]: + contextvar: contextvars.ContextVar[T] = contextvars.ContextVar(f"resource_context({resource_factory.__name__})") + + def with_resource(*args: P.args, **kwargs: P.kwargs): + @contextlib.asynccontextmanager + async def manager(): + resource = resource_factory(*args, **kwargs) + token = contextvar.set(resource) + try: + yield resource + finally: + _ = await getattr(resource, "aclose", noop)() + contextvar.reset(token) + + return manager() + + def get_resource() -> T: + try: + return contextvar.get() + except LookupError: + return default_resource_factory() + + return get_resource, with_resource + + +__all__ = ["resource_context"] diff --git a/apps/beeai-server/tests/e2e/conftest.py b/apps/beeai-server/tests/e2e/conftest.py index 1723e448d..ca7b03f9a 100644 --- a/apps/beeai-server/tests/e2e/conftest.py +++ b/apps/beeai-server/tests/e2e/conftest.py @@ -7,19 +7,15 @@ from typing import Any import httpx +import pytest import pytest_asyncio from a2a.client import A2AClient from a2a.types import AgentCard +from beeai_sdk.platform import Variables, with_client logger = logging.getLogger(__name__) -@pytest_asyncio.fixture() -async def api_client(test_configuration) -> AsyncIterator[httpx.AsyncClient]: - async with httpx.AsyncClient(base_url=test_configuration.server_url, timeout=None) as client: - yield client - - @pytest_asyncio.fixture() async def a2a_client_factory() -> Callable[[AgentCard | dict[str, Any]], AsyncIterator[A2AClient]]: @asynccontextmanager @@ -31,10 +27,18 @@ async def a2a_client_factory(agent_card: AgentCard | dict) -> AsyncIterator[A2AC @pytest_asyncio.fixture() -async def setup_real_llm(api_client, test_configuration): - env = { - "LLM_API_BASE": test_configuration.llm_api_base, - "LLM_API_KEY": test_configuration.llm_api_key.get_secret_value(), - "LLM_MODEL": test_configuration.llm_model, - } - await api_client.put("variables", json={"env": env}) +async def setup_platform_client(test_configuration) -> AsyncIterator[None]: + async with with_client(base_url=test_configuration.server_url, timeout=None): + yield None + + +@pytest_asyncio.fixture() +@pytest.mark.usefixtures("setup_platform_client") +async def setup_real_llm(test_configuration): + await Variables.save( + { + "LLM_API_BASE": test_configuration.llm_api_base, + "LLM_API_KEY": test_configuration.llm_api_key.get_secret_value(), + "LLM_MODEL": test_configuration.llm_model, + } + ) diff --git a/apps/beeai-server/tests/e2e/routes/test_files.py b/apps/beeai-server/tests/e2e/routes/test_files.py index c87cf9dca..349fd3fd0 100644 --- a/apps/beeai-server/tests/e2e/routes/test_files.py +++ b/apps/beeai-server/tests/e2e/routes/test_files.py @@ -13,26 +13,28 @@ @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_files(subtests, setup_real_llm, api_client): +@pytest.mark.usefixtures("clean_up", "setup_platform_client", "setup_real_llm") +async def test_files(subtests): with subtests.test("upload file"): file = await File.create( - filename="test.txt", content=b'{"hello": "world"}', content_type="application/json", client=api_client + filename="test.txt", + content=b'{"hello": "world"}', + content_type="application/json", ) file_id = file.id with subtests.test("get file metadata"): - retrieved_file = await File.get(file_id, client=api_client) + retrieved_file = await File.get(file_id) assert retrieved_file.id == file_id with subtests.test("get file content"): - content = await retrieved_file.content(client=api_client) + content = await retrieved_file.content() assert content == '{"hello": "world"}' with subtests.test("delete file"): - await File.delete(file_id, client=api_client) + await File.delete(file_id) with pytest.raises(httpx.HTTPStatusError, match="404 Not Found"): - await File.get(file_id, client=api_client) + _ = await File.get(self=file_id) @pytest.fixture @@ -58,8 +60,8 @@ def create_fn(text: str) -> BytesIO: @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_text_extraction_pdf_workflow(subtests, api_client, test_pdf: Callable[[str], BytesIO]): +@pytest.mark.usefixtures("clean_up", "setup_platform_client") +async def test_text_extraction_pdf_workflow(subtests, test_configuration, test_pdf: Callable[[str], BytesIO]): """Test complete PDF text extraction workflow: upload -> extract -> wait -> verify""" # Create a simple PDF-like content for testing @@ -69,23 +71,25 @@ async def test_text_extraction_pdf_workflow(subtests, api_client, test_pdf: Call with subtests.test("upload PDF file"): file = await File.create( - filename="test_document.pdf", content=pdf, content_type="application/pdf", client=api_client + filename="test_document.pdf", + content=pdf, + content_type="application/pdf", ) assert file.filename == "test_document.pdf" assert file.file_type == "user_upload" with subtests.test("create text extraction"): - extraction = await file.create_extraction(client=api_client) + extraction = await file.create_extraction() assert extraction.file_id == file.id assert extraction.status in ["pending", "in_progress", "completed"] with subtests.test("check extraction status"): - extraction = await file.get_extraction(client=api_client) + extraction = await file.get_extraction() assert extraction.file_id == file.id async for attempt in AsyncRetrying(stop=stop_after_delay(timedelta(seconds=40)), wait=wait_fixed(1)): with attempt: - extraction = await file.get_extraction(client=api_client) + extraction = await file.get_extraction() final_status = extraction.status if final_status not in ["completed", "failed"]: raise ValueError("not completed") @@ -95,41 +99,42 @@ async def test_text_extraction_pdf_workflow(subtests, api_client, test_pdf: Call assert extraction.finished_at is not None with subtests.test("verify extracted text content"): - content = await file.text_content(client=api_client) + content = await file.text_content() # Check that we get some text content back assert len(content) > 0, "No text content was extracted" assert "Beeai is the future of AI" in content with subtests.test("delete extraction"): - await file.delete_extraction(client=api_client) + await file.delete_extraction() with ( subtests.test("verify extraction deleted"), pytest.raises(httpx.HTTPStatusError, match="404 Not Found"), ): - await file.get_extraction(client=api_client) + _ = await file.get_extraction() @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_text_extraction_plain_text_workflow(subtests, setup_real_llm, api_client): +@pytest.mark.usefixtures("clean_up", "setup_real_llm", "setup_platform_client") +async def test_text_extraction_plain_text_workflow(subtests): """Test text extraction for plain text files (should be immediate)""" - text_content = "This is a sample text document with some content for testing text extraction." with subtests.test("upload text file"): file = await File.create( - filename="test_document.txt", content=text_content.encode(), content_type="text/plain", client=api_client + filename="test_document.txt", + content=text_content.encode(), + content_type="text/plain", ) assert file.filename == "test_document.txt" with subtests.test("create text extraction for plain text"): - extraction = await file.create_extraction(client=api_client) + extraction = await file.create_extraction() assert extraction.file_id == file.id # Plain text files should be completed immediately assert extraction.status == "completed" with subtests.test("verify immediate text content access"): - extracted_content = await file.text_content(client=api_client) + extracted_content = await file.text_content() assert extracted_content == text_content diff --git a/apps/beeai-server/tests/e2e/routes/test_vector_stores.py b/apps/beeai-server/tests/e2e/routes/test_vector_stores.py index ea156cefa..35e40198d 100644 --- a/apps/beeai-server/tests/e2e/routes/test_vector_stores.py +++ b/apps/beeai-server/tests/e2e/routes/test_vector_stores.py @@ -9,8 +9,8 @@ @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_vector_stores(subtests, api_client): +@pytest.mark.usefixtures("clean_up", "setup_platform_client") +async def test_vector_stores(subtests): items = [ VectorStoreItem( document_id="doc_001", @@ -43,19 +43,22 @@ async def test_vector_stores(subtests, api_client): name="test-vector-store", dimension=128, model_id="custom_model_id", - client=api_client, ) with subtests.test("upload vectors"): - await vector_store.add_documents(items, client=api_client) + await vector_store.add_documents( + items, + ) with subtests.test("verify usage_bytes updated after upload"): - await vector_store.get(client=api_client) + await vector_store.get() usage_bytes = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes > 0, "Usage bytes should be greater than 0 after uploading vectors" with subtests.test("search vectors"): - search_results = await vector_store.search(query_vector=[1.0] * 127 + [1.0], client=api_client) + search_results = await vector_store.search( + query_vector=[1.0] * 127 + [1.0], + ) # Check that each result has the new structure with item and score for result in search_results: @@ -71,8 +74,8 @@ async def test_vector_stores(subtests, api_client): @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_vector_store_deletion(subtests, api_client): +@pytest.mark.usefixtures("clean_up", "setup_platform_client") +async def test_vector_store_deletion(subtests): """Test vector store deletion functionality""" items = [ VectorStoreItem( @@ -86,34 +89,33 @@ async def test_vector_store_deletion(subtests, api_client): ] with subtests.test("create vector store for deletion test"): - vector_store = await VectorStore.create( - name="test-deletion-store", - dimension=128, - model_id="custom_model_id", - client=api_client, - ) + vector_store = await VectorStore.create(name="test-deletion-store", dimension=128, model_id="custom_model_id") vector_store_id = vector_store.id with subtests.test("add items to vector store"): - await vector_store.add_documents(items, client=api_client) + await vector_store.add_documents( + items, + ) with subtests.test("verify vector store exists before deletion"): - await vector_store.get(client=api_client) + await vector_store.get() assert vector_store.id == vector_store_id with subtests.test("delete vector store"): - await vector_store.delete(client=api_client) + await vector_store.delete() with ( subtests.test("verify vector store is deleted"), pytest.raises(httpx.HTTPStatusError, match="404 Not Found"), ): - await VectorStore.get(str(vector_store_id), client=api_client) + await VectorStore.get( + str(vector_store_id), + ) @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_document_deletion(subtests, api_client): +@pytest.mark.usefixtures("clean_up", "setup_platform_client") +async def test_document_deletion(subtests): """Test individual document deletion functionality""" initial_items = [ VectorStoreItem( @@ -143,36 +145,41 @@ async def test_document_deletion(subtests, api_client): ] with subtests.test("create vector store"): - vector_store = await VectorStore.create( - name="test-doc-deletion", - dimension=128, - model_id="custom_model_id", - client=api_client, - ) + vector_store = await VectorStore.create(name="test-doc-deletion", dimension=128, model_id="custom_model_id") with subtests.test("add initial documents"): - await vector_store.add_documents(initial_items, client=api_client) + await vector_store.add_documents( + initial_items, + ) with subtests.test("verify all documents exist via search and track usage_bytes"): - search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + search_results = await vector_store.search( + query_vector=[1.0] * 128, + limit=10, + ) assert len(search_results) == 3 - await vector_store.get(client=api_client) + await vector_store.get() usage_bytes_before_deletion = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_before_deletion > 0, "Usage bytes should be greater than 0 after adding documents" with subtests.test("delete specific document"): - await vector_store.delete_document("doc_002", client=api_client) + await vector_store.delete_document( + "doc_002", + ) with subtests.test("verify document was deleted and usage_bytes decreased"): - search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + search_results = await vector_store.search( + query_vector=[1.0] * 128, + limit=10, + ) assert len(search_results) == 2 document_ids = [result.item.document_id for result in search_results] assert "doc_002" not in document_ids assert "doc_001" in document_ids assert "doc_003" in document_ids - await vector_store.get(client=api_client) + await vector_store.get() usage_bytes_after_deletion = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_after_deletion < usage_bytes_before_deletion, ( "Usage bytes should decrease after deleting a document" @@ -180,8 +187,8 @@ async def test_document_deletion(subtests, api_client): @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_adding_items_to_existing_documents(subtests, api_client): +@pytest.mark.usefixtures("clean_up", "setup_platform_client") +async def test_adding_items_to_existing_documents(subtests): """Test adding new items to existing documents in vector store""" initial_items = [ VectorStoreItem( @@ -222,37 +229,42 @@ async def test_adding_items_to_existing_documents(subtests, api_client): ] with subtests.test("create vector store"): - vector_store = await VectorStore.create( - name="test-add-items", - dimension=128, - model_id="custom_model_id", - client=api_client, - ) + vector_store = await VectorStore.create(name="test-add-items", dimension=128, model_id="custom_model_id") with subtests.test("verify initial vector store usage_bytes is 0"): - await vector_store.get(client=api_client) + await vector_store.get() initial_usage_bytes = vector_store.stats.usage_bytes if vector_store.stats else 0 assert initial_usage_bytes == 0 with subtests.test("add initial items"): - await vector_store.add_documents(initial_items, client=api_client) + await vector_store.add_documents( + initial_items, + ) with subtests.test("verify initial items count and usage_bytes updated"): - search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + search_results = await vector_store.search( + query_vector=[1.0] * 128, + limit=10, + ) assert len(search_results) == 2 - await vector_store.get(client=api_client) + await vector_store.get() usage_bytes_after_initial = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_after_initial > 0, "Usage bytes should be greater than 0 after adding items" with subtests.test("add additional items to existing and new documents"): - await vector_store.add_documents(additional_items, client=api_client) + await vector_store.add_documents( + additional_items, + ) with subtests.test("verify all items are present and usage_bytes increased"): - search_results = await vector_store.search(query_vector=[1.0] * 128, limit=10, client=api_client) + search_results = await vector_store.search( + query_vector=[1.0] * 128, + limit=10, + ) assert len(search_results) == 4 - await vector_store.get(client=api_client) + await vector_store.get() usage_bytes_after_additional = vector_store.stats.usage_bytes if vector_store.stats else 0 assert usage_bytes_after_additional > usage_bytes_after_initial, ( "Usage bytes should increase after adding more items" @@ -271,8 +283,8 @@ async def test_adding_items_to_existing_documents(subtests, api_client): @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_document_listing(subtests, api_client): +@pytest.mark.usefixtures("clean_up", "setup_platform_client") +async def test_document_listing(subtests): """Test listing documents in a vector store""" items = [ VectorStoreItem( @@ -302,15 +314,12 @@ async def test_document_listing(subtests, api_client): ] with subtests.test("create vector store"): - vector_store = await VectorStore.create( - name="test-doc-listing", - dimension=128, - model_id="custom_model_id", - client=api_client, - ) + vector_store = await VectorStore.create(name="test-doc-listing", dimension=128, model_id="custom_model_id") with subtests.test("add items to vector store"): - await vector_store.add_documents(items, client=api_client) + await vector_store.add_documents( + items, + ) with subtests.test("list documents in vector store"): - assert {doc.id for doc in await vector_store.list_documents(client=api_client)} == {"doc_001", "doc_002"} + assert {doc.id for doc in await vector_store.list_documents()} == {"doc_001", "doc_002"} diff --git a/apps/beeai-server/tests/e2e/test_agent_starts.py b/apps/beeai-server/tests/e2e/test_agent_starts.py index c8e226867..f819f3410 100644 --- a/apps/beeai-server/tests/e2e/test_agent_starts.py +++ b/apps/beeai-server/tests/e2e/test_agent_starts.py @@ -6,37 +6,36 @@ import pytest from a2a.types import ( - AgentCard, Message, MessageSendParams, + Part, Role, SendMessageRequest, SendMessageSuccessResponse, TextPart, ) +from beeai_sdk.platform import Provider @pytest.mark.e2e @pytest.mark.asyncio -@pytest.mark.usefixtures("clean_up") -async def test_agent(subtests, setup_real_llm, api_client, a2a_client_factory): +@pytest.mark.usefixtures("clean_up", "setup_real_llm", "setup_platform_client") +async def test_agent(subtests, a2a_client_factory): agent_image = "ghcr.io/i-am-bee/beeai-platform-agent-starter/my-agent-a2a:latest" with subtests.test("add chat agent"): - response = await api_client.post("api/v1/providers", json={"location": agent_image}) - response.raise_for_status() - providers_response = await api_client.get("api/v1/providers") - providers_response.raise_for_status() - providers = providers_response.json() - assert len(providers["items"]) == 1 - assert providers["items"][0]["source"] == agent_image - agent_card = AgentCard.model_validate(providers["items"][0]["agent_card"]) - assert agent_card + _ = await Provider.create(location=agent_image) + providers = await Provider.list() + assert len(providers) == 1 + assert providers[0].source == agent_image + assert providers[0].agent_card - async with a2a_client_factory(agent_card) as a2a_client: + async with a2a_client_factory(providers[0].agent_card) as a2a_client: with subtests.test("run chat agent for the first time"): num_parallel = 3 message = Message( - messageId=str(uuid4()), parts=[TextPart(text="Repeat this exactly: 'hello world'")], role=Role.user + message_id=str(uuid4()), + parts=[Part(root=TextPart(text="Repeat this exactly: 'hello world'"))], + role=Role.user, ) response = await a2a_client.send_message( SendMessageRequest(id=str(uuid4()), params=MessageSendParams(message=message)) From e13a560560bac818247becf0b225111a7610bfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= Date: Wed, 6 Aug 2025 14:14:20 +0200 Subject: [PATCH 5/5] refactor: renaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- .../src/beeai_sdk/platform/context.py | 9 +++--- apps/beeai-sdk/src/beeai_sdk/platform/file.py | 28 +++++++++++++------ .../src/beeai_sdk/platform/provider.py | 14 ++++++---- .../src/beeai_sdk/platform/variables.py | 6 ++-- .../src/beeai_sdk/platform/vector_store.py | 16 +++++------ .../src/beeai_sdk/util/resource_context.py | 14 +++++----- apps/beeai-server/tests/e2e/conftest.py | 4 +-- 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/context.py b/apps/beeai-sdk/src/beeai_sdk/platform/context.py index 08d207bc5..29cccc26d 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/context.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/context.py @@ -7,11 +7,12 @@ from beeai_sdk.util import resource_context -get_client, with_client = resource_context( - httpx.AsyncClient, lambda: httpx.AsyncClient(base_url=os.environ.get("PLATFORM_URL", "http://127.0.0.1:8333")) +get_platform_client, use_platform_client = resource_context( + factory=httpx.AsyncClient, + default_factory=lambda: httpx.AsyncClient(base_url=os.environ.get("PLATFORM_URL", "http://127.0.0.1:8333")), ) __all__ = [ - "get_client", - "with_client", + "get_platform_client", + "use_platform_client", ] diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/file.py b/apps/beeai-sdk/src/beeai_sdk/platform/file.py index 9b381a13a..dcb9fa5c4 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/file.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/file.py @@ -8,7 +8,7 @@ import httpx import pydantic -from beeai_sdk.platform.context import get_client +from beeai_sdk.platform.context import get_platform_client class Extraction(pydantic.BaseModel): @@ -43,7 +43,7 @@ async def create( ) -> File: return pydantic.TypeAdapter(File).validate_python( ( - await (client or get_client()).post( + await (client or get_platform_client()).post( url="/api/v1/files", files={"file": (filename, content, content_type)}, ) @@ -60,7 +60,7 @@ async def get( # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `File.get("123")` to obtain a new instance file_id = self if isinstance(self, str) else self.id return pydantic.TypeAdapter(File).validate_python( - (await (client or get_client()).get(url=f"/api/v1/files/{file_id}")).raise_for_status().json() + (await (client or get_platform_client()).get(url=f"/api/v1/files/{file_id}")).raise_for_status().json() ) async def delete( @@ -70,7 +70,7 @@ async def delete( ) -> None: # `self` has a weird type so that you can call both `instance.delete()` or `File.delete("123")` file_id = self if isinstance(self, str) else self.id - _ = (await (client or get_client()).delete(url=f"/api/v1/files/{file_id}")).raise_for_status() + _ = (await (client or get_platform_client()).delete(url=f"/api/v1/files/{file_id}")).raise_for_status() async def content( self: File | str, @@ -79,7 +79,11 @@ async def content( ) -> str: # `self` has a weird type so that you can call both `instance.content()` to get content of an instance, or `File.content("123")` file_id = self if isinstance(self, str) else self.id - return (await (client or get_client()).get(url=f"/api/v1/files/{file_id}/content")).raise_for_status().text + return ( + (await (client or get_platform_client()).get(url=f"/api/v1/files/{file_id}/content")) + .raise_for_status() + .text + ) async def text_content( self: File | str, @@ -88,7 +92,11 @@ async def text_content( ) -> str: # `self` has a weird type so that you can call both `instance.text_content()` to get text content of an instance, or `File.text_content("123")` file_id = self if isinstance(self, str) else self.id - return (await (client or get_client()).get(url=f"/api/v1/files/{file_id}/text_content")).raise_for_status().text + return ( + (await (client or get_platform_client()).get(url=f"/api/v1/files/{file_id}/text_content")) + .raise_for_status() + .text + ) async def create_extraction( self: File | str, @@ -99,7 +107,7 @@ async def create_extraction( file_id = self if isinstance(self, str) else self.id return pydantic.TypeAdapter(Extraction).validate_python( ( - await (client or get_client()).post( + await (client or get_platform_client()).post( url=f"/api/v1/files/{file_id}/extraction", ) ) @@ -116,7 +124,7 @@ async def get_extraction( file_id = self if isinstance(self, str) else self.id return pydantic.TypeAdapter(Extraction).validate_python( ( - await (client or get_client()).get( + await (client or get_platform_client()).get( url=f"/api/v1/files/{file_id}/extraction", ) ) @@ -131,4 +139,6 @@ async def delete_extraction( ) -> None: # `self` has a weird type so that you can call both `instance.delete_extraction()` or `File.delete_extraction("123", "456")` file_id = self if isinstance(self, str) else self.id - _ = (await (client or get_client()).delete(url=f"/api/v1/files/{file_id}/extraction")).raise_for_status() + _ = ( + await (client or get_platform_client()).delete(url=f"/api/v1/files/{file_id}/extraction") + ).raise_for_status() diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/provider.py b/apps/beeai-sdk/src/beeai_sdk/platform/provider.py index 9da048472..a86b6058d 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/provider.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/provider.py @@ -8,7 +8,7 @@ import pydantic from a2a.types import AgentCard -from beeai_sdk.platform.context import get_client +from beeai_sdk.platform.context import get_platform_client class ProviderErrorMessage(pydantic.BaseModel): @@ -44,7 +44,7 @@ async def create( ) -> "Provider": return pydantic.TypeAdapter(Provider).validate_python( ( - await (client or get_client()).post( + await (client or get_platform_client()).post( url="/api/v1/providers", json={ "location": location, @@ -66,7 +66,7 @@ async def preview( ) -> "Provider": return pydantic.TypeAdapter(Provider).validate_python( ( - await (client or get_client()).post( + await (client or get_platform_client()).post( url="/api/v1/providers/preview", json={ "location": location, @@ -82,7 +82,9 @@ async def get(self: "Provider | str", /, *, client: httpx.AsyncClient | None = N # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `Provider.get("123")` to obtain a new instance provider_id = self if isinstance(self, str) else self.id result = pydantic.TypeAdapter(Provider).validate_json( - (await (client or get_client()).get(url=f"/api/v1/providers/{provider_id}")).raise_for_status().content + (await (client or get_platform_client()).get(url=f"/api/v1/providers/{provider_id}")) + .raise_for_status() + .content ) if isinstance(self, Provider): self.__dict__.update(result.__dict__) @@ -92,10 +94,10 @@ async def get(self: "Provider | str", /, *, client: httpx.AsyncClient | None = N async def delete(self: "Provider | str", /, *, client: httpx.AsyncClient | None = None) -> None: # `self` has a weird type so that you can call both `instance.delete()` or `Provider.delete("123")` provider_id = self if isinstance(self, str) else self.id - _ = (await (client or get_client()).delete(f"/api/v1/providers/{provider_id}")).raise_for_status() + _ = (await (client or get_platform_client()).delete(f"/api/v1/providers/{provider_id}")).raise_for_status() @staticmethod async def list(*, client: httpx.AsyncClient | None = None) -> list["Provider"]: return pydantic.TypeAdapter(list[Provider]).validate_python( - (await (client or get_client()).get(url="/api/v1/providers")).raise_for_status().json()["items"] + (await (client or get_platform_client()).get(url="/api/v1/providers")).raise_for_status().json()["items"] ) diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/variables.py b/apps/beeai-sdk/src/beeai_sdk/platform/variables.py index d682f69b4..04f786edf 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/variables.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/variables.py @@ -5,7 +5,7 @@ import httpx -from beeai_sdk.platform.context import get_client +from beeai_sdk.platform.context import get_platform_client class Variables(dict[str, str]): @@ -21,7 +21,7 @@ async def save( ...or as an instance method: variables.save() """ _ = ( - await (client or get_client()).put( + await (client or get_platform_client()).put( url="/api/v1/variables", json={"env": self}, ) @@ -35,7 +35,7 @@ async def load(self: Variables | None = None, *, client: httpx.AsyncClient | Non ...or as an instance method to update the instance: variables.load() """ new_variables: dict[str, str] = ( - (await (client or get_client()).get(url="/api/v1/variables")).raise_for_status().json() + (await (client or get_platform_client()).get(url="/api/v1/variables")).raise_for_status().json() ) if isinstance(self, Variables): self.clear() diff --git a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py index 1c0e973d4..024887b67 100644 --- a/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py +++ b/apps/beeai-sdk/src/beeai_sdk/platform/vector_store.py @@ -10,7 +10,7 @@ import httpx import pydantic -from beeai_sdk.platform.context import get_client +from beeai_sdk.platform.context import get_platform_client def validate_metadata(metadata: dict[str, str]) -> dict[str, str]: @@ -88,7 +88,7 @@ async def create( ) -> VectorStore: return pydantic.TypeAdapter(VectorStore).validate_json( ( - await (client or get_client()).post( + await (client or get_platform_client()).post( url="/api/v1/vector_stores", json={"name": name, "dimension": dimension, "model_id": model_id}, ) @@ -107,7 +107,7 @@ async def get( vector_store_id = self if isinstance(self, str) else self.id result = pydantic.TypeAdapter(VectorStore).validate_json( ( - await (client or get_client()).get( + await (client or get_platform_client()).get( url=f"/api/v1/vector_stores/{vector_store_id}", ) ) @@ -128,7 +128,7 @@ async def delete( # `self` has a weird type so that you can call both `instance.delete()` or `VectorStore.delete("123")` vector_store_id = self if isinstance(self, str) else self.id _ = ( - await (client or get_client()).delete( + await (client or get_platform_client()).delete( url=f"/api/v1/vector_stores/{vector_store_id}", ) ).raise_for_status() @@ -139,7 +139,7 @@ async def add_documents( # `self` has a weird type so that you can call both `instance.add_documents()` or `VectorStore.add_documents("123", items)` vector_store_id = self if isinstance(self, str) else self.id _ = ( - await (client or get_client()).put( + await (client or get_platform_client()).put( url=f"/api/v1/vector_stores/{vector_store_id}", json=[item.model_dump(mode="json") for item in items], ) @@ -157,7 +157,7 @@ async def search( vector_store_id = self if isinstance(self, str) else self.id return pydantic.TypeAdapter(list[VectorStoreSearchResult]).validate_python( ( - await (client or get_client()).post( + await (client or get_platform_client()).post( url=f"/api/v1/vector_stores/{vector_store_id}/search", json={"query_vector": query_vector, "limit": limit}, ) @@ -175,7 +175,7 @@ async def list_documents( # `self` has a weird type so that you can call both `instance.list_documents()` to list documents in an instance, or `VectorStore.list_documents("123")` vector_store_id = self if isinstance(self, str) else self.id return pydantic.TypeAdapter(list[VectorStoreDocument]).validate_python( - (await (client or get_client()).get(url=f"/api/v1/vector_stores/{vector_store_id}/documents")) + (await (client or get_platform_client()).get(url=f"/api/v1/vector_stores/{vector_store_id}/documents")) .raise_for_status() .json()["items"] ) @@ -190,7 +190,7 @@ async def delete_document( # `self` has a weird type so that you can call both `instance.delete_document()` or `VectorStore.delete_document("123", "456")` vector_store_id = self if isinstance(self, str) else self.id _ = ( - await (client or get_client()).delete( + await (client or get_platform_client()).delete( url=f"/api/v1/vector_stores/{vector_store_id}/documents/{document_id}", ) ).raise_for_status() diff --git a/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py b/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py index 0114d97c5..52e4f5647 100644 --- a/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py +++ b/apps/beeai-sdk/src/beeai_sdk/util/resource_context.py @@ -15,15 +15,15 @@ async def noop(): def resource_context( - resource_factory: typing.Callable[P, T], - default_resource_factory: typing.Callable[[], T], + factory: typing.Callable[P, T], + default_factory: typing.Callable[[], T], ) -> tuple[typing.Callable[[], T], typing.Callable[P, contextlib.AbstractAsyncContextManager[T]]]: - contextvar: contextvars.ContextVar[T] = contextvars.ContextVar(f"resource_context({resource_factory.__name__})") + contextvar: contextvars.ContextVar[T] = contextvars.ContextVar(f"resource_context({factory.__name__})") - def with_resource(*args: P.args, **kwargs: P.kwargs): + def use_resource(*args: P.args, **kwargs: P.kwargs): @contextlib.asynccontextmanager async def manager(): - resource = resource_factory(*args, **kwargs) + resource = factory(*args, **kwargs) token = contextvar.set(resource) try: yield resource @@ -37,9 +37,9 @@ def get_resource() -> T: try: return contextvar.get() except LookupError: - return default_resource_factory() + return default_factory() - return get_resource, with_resource + return get_resource, use_resource __all__ = ["resource_context"] diff --git a/apps/beeai-server/tests/e2e/conftest.py b/apps/beeai-server/tests/e2e/conftest.py index ca7b03f9a..c248a82cc 100644 --- a/apps/beeai-server/tests/e2e/conftest.py +++ b/apps/beeai-server/tests/e2e/conftest.py @@ -11,7 +11,7 @@ import pytest_asyncio from a2a.client import A2AClient from a2a.types import AgentCard -from beeai_sdk.platform import Variables, with_client +from beeai_sdk.platform import Variables, use_platform_client logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ async def a2a_client_factory(agent_card: AgentCard | dict) -> AsyncIterator[A2AC @pytest_asyncio.fixture() async def setup_platform_client(test_configuration) -> AsyncIterator[None]: - async with with_client(base_url=test_configuration.server_url, timeout=None): + async with use_platform_client(base_url=test_configuration.server_url, timeout=None): yield None