From e95c56ff8d83bd120d52e94e961dc318c67e41c1 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 12:17:14 +0545 Subject: [PATCH 01/20] add workflow/testclient --- .github/workflows/run-backend.yml | 60 +++++++++++++++++++ autonomous_agent_api/backend/app/asgi.py | 24 +++++++- .../tests/app/controllers/test_ping.py | 4 +- 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/run-backend.yml diff --git a/.github/workflows/run-backend.yml b/.github/workflows/run-backend.yml new file mode 100644 index 000000000..d7b843b66 --- /dev/null +++ b/.github/workflows/run-backend.yml @@ -0,0 +1,60 @@ + +name: Basic Workflow test / Docker Image Builder + +on: + push: + branches: ["master", "dev"] + pull_request: + branches: ["master", "dev"] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Enable Access for Branch for Workflow + uses: actions/checkout@v2 + with: + submodules: "recursive" + + - name: setup python + uses: actions/setup-python@v2 + with: + python-version: 3.12.2 + + - name: Install Poetry + working-directory: autonomous_agent_api + run: pip install poetry==1.8.2 + + - name: Install Dependencies + working-directory: autonomous_agent_api + run: | + poetry install + poetry run prisma generate + + - name: Run test cases + working-directory: autonomous_agent_api + env: + DATABASE_URL : "" + run: poetry run pytest -m ping + + build_on_pr: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes : 30 + steps: + - name: Checkout code with submodules + uses: actions/checkout@v2 + with: + submodules: "recursive" + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + push: false + tags: backend:latest + context: autonomous_agent_api diff --git a/autonomous_agent_api/backend/app/asgi.py b/autonomous_agent_api/backend/app/asgi.py index 702d1be4f..0b828853a 100644 --- a/autonomous_agent_api/backend/app/asgi.py +++ b/autonomous_agent_api/backend/app/asgi.py @@ -26,10 +26,11 @@ async def lifespan(app: FastAPI): logger.info("Starting Server") - await prisma_connection.connect() + if type(app , get_application): + await prisma_connection.connect() yield log.debug("Execute FastAPI shutdown event handler.") - # Gracefully close utilities. + # Gracefully close utilities. if settings.USE_REDIS: await RedisClient.close_redis_client() @@ -60,3 +61,22 @@ def get_application() -> FastAPI: app.add_exception_handler(HTTPException, http_exception_handler) return app + + +def get_test_application() -> FastAPI: + """ + Initialize FastApi application for testing + """ + logging.info("Setting up Test Environment") + app = FastAPI( + title=settings.PROJECT_NAME, + debug=settings.DEBUG, + version=settings.VERSION, + docs_url=settings.DOCS_URL, + lifespan=lifespan, + ) + log.debug("Add application routes.") + app.include_router(root_api_router) + log.debug("Register global exception handler for custom HTTPException.") + app.add_exception_handler(HTTPException, http_exception_handler) + return app \ No newline at end of file diff --git a/autonomous_agent_api/tests/app/controllers/test_ping.py b/autonomous_agent_api/tests/app/controllers/test_ping.py index b09643121..32d1006cb 100644 --- a/autonomous_agent_api/tests/app/controllers/test_ping.py +++ b/autonomous_agent_api/tests/app/controllers/test_ping.py @@ -1,9 +1,9 @@ -from backend.app.asgi import get_application +from backend.app.asgi import get_test_application from fastapi.testclient import TestClient from fastapi import status import pytest -client = TestClient(get_application()) +client = TestClient(get_test_application) @pytest.mark.ping From 85bd901dfbb5333e946a728395679e07e3479af7 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 12:57:23 +0545 Subject: [PATCH 02/20] add get test application for testing --- autonomous_agent_api/backend/app/asgi.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/autonomous_agent_api/backend/app/asgi.py b/autonomous_agent_api/backend/app/asgi.py index 0b828853a..0b78b0d36 100644 --- a/autonomous_agent_api/backend/app/asgi.py +++ b/autonomous_agent_api/backend/app/asgi.py @@ -1,5 +1,6 @@ """Application implementation - ASGI.""" +import os import logging from contextlib import asynccontextmanager @@ -26,8 +27,9 @@ async def lifespan(app: FastAPI): logger.info("Starting Server") - if type(app , get_application): - await prisma_connection.connect() + if not os.environ.get('TESTING'): #check if its a testing enironment , if not then connect to databse + await prisma_connection.connect() + yield log.debug("Execute FastAPI shutdown event handler.") # Gracefully close utilities. @@ -36,7 +38,10 @@ async def lifespan(app: FastAPI): await AiohttpClient.close_aiohttp_client() - await prisma_connection.disconnect() + + if not os.environ.get('TESTING'): #check if it's not a testing env , if not close databse connection + await prisma_connection.disconnect() + logger.info("Stopping Server") @@ -66,7 +71,13 @@ def get_application() -> FastAPI: def get_test_application() -> FastAPI: """ Initialize FastApi application for testing + + Returns: + FastAPI : Application object instance + """ + os.environ.__setattr__('TESTING' , True) + logging.info("Setting up Test Environment") app = FastAPI( title=settings.PROJECT_NAME, From 685c4e30c280b516c395c543c65200f2243f4bbc Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 13:11:11 +0545 Subject: [PATCH 03/20] update workflow to run github actions marked test --- .github/workflows/run-backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend.yml b/.github/workflows/run-backend.yml index d7b843b66..97d21ab86 100644 --- a/.github/workflows/run-backend.yml +++ b/.github/workflows/run-backend.yml @@ -36,7 +36,7 @@ jobs: working-directory: autonomous_agent_api env: DATABASE_URL : "" - run: poetry run pytest -m ping + run: poetry run pytest -m github_actions build_on_pr: if: github.event_name == 'pull_request' From 796ecae30e01d7f73509369a2a7710210bca1234 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 13:12:15 +0545 Subject: [PATCH 04/20] add pytest markers --- autonomous_agent_api/pyproject.toml | 8 +++++++- autonomous_agent_api/tests/app/controllers/test_ping.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/autonomous_agent_api/pyproject.toml b/autonomous_agent_api/pyproject.toml index ca46b28d4..66b87915f 100644 --- a/autonomous_agent_api/pyproject.toml +++ b/autonomous_agent_api/pyproject.toml @@ -97,4 +97,10 @@ module = [ "gunicorn.*", "redis.*", ] -ignore_missing_imports = true \ No newline at end of file +ignore_missing_imports = true + +[tool.pytest.ini_options] +markers = [ + "ping: run demo test", + "github_actions: Tests for Github actions", +] diff --git a/autonomous_agent_api/tests/app/controllers/test_ping.py b/autonomous_agent_api/tests/app/controllers/test_ping.py index 32d1006cb..613f8457c 100644 --- a/autonomous_agent_api/tests/app/controllers/test_ping.py +++ b/autonomous_agent_api/tests/app/controllers/test_ping.py @@ -3,10 +3,10 @@ from fastapi import status import pytest -client = TestClient(get_test_application) +client = TestClient(get_test_application()) -@pytest.mark.ping +@pytest.mark.github_actions def test_ping(): response = client.get("/api/ping") assert response.status_code == status.HTTP_200_OK From 49a11e6c0d7eca95e4e4cca006f2e869ded7925a Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 13:15:15 +0545 Subject: [PATCH 05/20] remove main branch from pull reqeust in workflow --- .github/workflows/run-backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend.yml b/.github/workflows/run-backend.yml index 97d21ab86..eb5bdb27a 100644 --- a/.github/workflows/run-backend.yml +++ b/.github/workflows/run-backend.yml @@ -5,7 +5,7 @@ on: push: branches: ["master", "dev"] pull_request: - branches: ["master", "dev"] + branches: ["dev"] jobs: build: From 50561f81d3a8867c521dc7ed6f72bf6ce689f063 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 13:26:30 +0545 Subject: [PATCH 06/20] update poetry lock --- .github/workflows/run-backend.yml | 2 +- autonomous_agent_api/poetry.lock | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-backend.yml b/.github/workflows/run-backend.yml index eb5bdb27a..cbcd780b1 100644 --- a/.github/workflows/run-backend.yml +++ b/.github/workflows/run-backend.yml @@ -8,7 +8,7 @@ on: branches: ["dev"] jobs: - build: + run_backend_tests: runs-on: ubuntu-latest steps: diff --git a/autonomous_agent_api/poetry.lock b/autonomous_agent_api/poetry.lock index 27389cf91..d9b0aac64 100644 --- a/autonomous_agent_api/poetry.lock +++ b/autonomous_agent_api/poetry.lock @@ -363,6 +363,21 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "classy-fastapi" +version = "0.6.1" +description = "Class based routing for FastAPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "classy-fastapi-0.6.1.tar.gz", hash = "sha256:5dfc33bab8e01e07c56855b78ce9a8152c871ab544a565d0d3d05a5c1ca4ed68"}, + {file = "classy_fastapi-0.6.1-py3-none-any.whl", hash = "sha256:196e5c2890269627d52851f3f86001a0dfda0070053d38f8a7bd896ac2f67737"}, +] + +[package.dependencies] +fastapi = ">=0.73.0,<1.0.0" +pydantic = ">=1.10.2,<3.0.0" + [[package]] name = "click" version = "8.1.7" @@ -2432,4 +2447,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ee6533f48aa232f19dc21dab76dce4b59f0b52c4097884fd647b18a0ae502807" +content-hash = "5fc1a77c6954037702b82110f3c48616bbe266ccc674a36e7df6a1232c671c45" From 1523ba34ce5824da8872a1d9ab93f11c180007fc Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 13:33:46 +0545 Subject: [PATCH 07/20] run black lint --- app.log | 21 ++++++++++++ autonomous_agent_api/backend/app/asgi.py | 23 +++++++------ .../backend/app/controllers/agent_router.py | 8 +++-- .../backend/app/controllers/ready.py | 4 ++- .../backend/app/exceptions/http.py | 4 ++- .../app/repositories/agent_repository.py | 32 ++++++++++++++----- .../backend/app/services/agent_service.py | 4 ++- .../backend/app/utils/redis.py | 4 ++- .../backend/app/views/error.py | 4 ++- .../unit/app/utils/test_aiohttp_client.py | 20 +++++++++--- .../tests/unit/cli/test_cli.py | 4 ++- .../tests/unit/cli/test_serve.py | 4 ++- 12 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 app.log diff --git a/app.log b/app.log new file mode 100644 index 000000000..7bc6b2d92 --- /dev/null +++ b/app.log @@ -0,0 +1,21 @@ +2024-03-29 12:45:59 - INFO - Setting up Test Environment +2024-03-29 12:46:00 - ERROR - Invalid value for workers: None +2024-03-29 12:46:00 - INFO - Starting Server +2024-03-29 12:46:10 - CRITICAL - Error Connecting to Database: Could not connect to the query engine +2024-03-29 12:46:10 - INFO - Starting Server +2024-03-29 12:46:24 - INFO - Setting up Test Environment +2024-03-29 12:46:24 - ERROR - Invalid value for workers: None +2024-03-29 12:46:24 - INFO - Starting Server +2024-03-29 12:46:43 - INFO - Setting up Test Environment +2024-03-29 12:54:00 - INFO - Setting up Test Environment +2024-03-29 12:54:00 - ERROR - Invalid value for workers: None +2024-03-29 12:54:00 - INFO - Starting Server +2024-03-29 12:54:07 - INFO - Setting up Test Environment +2024-03-29 12:54:25 - INFO - Setting up Test Environment +2024-03-29 12:54:25 - ERROR - Invalid value for workers: None +2024-03-29 12:54:25 - INFO - Starting Server +2024-03-29 12:54:32 - INFO - Setting up Test Environment +2024-03-29 12:59:42 - INFO - Setting up Test Environment +2024-03-29 12:59:42 - ERROR - Invalid value for workers: None +2024-03-29 12:59:42 - INFO - Starting Server +2024-03-29 12:59:52 - INFO - Setting up Test Environment diff --git a/autonomous_agent_api/backend/app/asgi.py b/autonomous_agent_api/backend/app/asgi.py index 0b78b0d36..8107be911 100644 --- a/autonomous_agent_api/backend/app/asgi.py +++ b/autonomous_agent_api/backend/app/asgi.py @@ -27,20 +27,23 @@ async def lifespan(app: FastAPI): logger.info("Starting Server") - if not os.environ.get('TESTING'): #check if its a testing enironment , if not then connect to databse + if not os.environ.get( + "TESTING" + ): # check if its a testing enironment , if not then connect to databse await prisma_connection.connect() yield log.debug("Execute FastAPI shutdown event handler.") - # Gracefully close utilities. + # Gracefully close utilities. if settings.USE_REDIS: await RedisClient.close_redis_client() await AiohttpClient.close_aiohttp_client() - - if not os.environ.get('TESTING'): #check if it's not a testing env , if not close databse connection - await prisma_connection.disconnect() + if not os.environ.get( + "TESTING" + ): # check if it's not a testing env , if not close databse connection + await prisma_connection.disconnect() logger.info("Stopping Server") @@ -70,13 +73,13 @@ def get_application() -> FastAPI: def get_test_application() -> FastAPI: """ - Initialize FastApi application for testing + Initialize FastApi application for testing - Returns: - FastAPI : Application object instance + Returns: + FastAPI : Application object instance """ - os.environ.__setattr__('TESTING' , True) + os.environ.__setattr__("TESTING", True) logging.info("Setting up Test Environment") app = FastAPI( @@ -90,4 +93,4 @@ def get_test_application() -> FastAPI: app.include_router(root_api_router) log.debug("Register global exception handler for custom HTTPException.") app.add_exception_handler(HTTPException, http_exception_handler) - return app \ No newline at end of file + return app diff --git a/autonomous_agent_api/backend/app/controllers/agent_router.py b/autonomous_agent_api/backend/app/controllers/agent_router.py index c6b38dcbf..15a8e7aae 100644 --- a/autonomous_agent_api/backend/app/controllers/agent_router.py +++ b/autonomous_agent_api/backend/app/controllers/agent_router.py @@ -11,7 +11,9 @@ class AgentRouter(Routable): - def __init__(self, agent_service: AgentService = get_agent_service(), *args, **kwargs): + def __init__( + self, agent_service: AgentService = get_agent_service(), *args, **kwargs + ): super().__init__(*args, **kwargs) self.agent_service = agent_service @@ -21,7 +23,9 @@ async def create_agent(self, agent_data: AgentCreateDTO): agent = await self.agent_service.create_agent(agent_data) return agent except Exception as e: - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) + ) @get("/agents/", response_model=List[AgentResponse]) async def list_agents(self): diff --git a/autonomous_agent_api/backend/app/controllers/ready.py b/autonomous_agent_api/backend/app/controllers/ready.py index 17bbf8928..7c1b79216 100644 --- a/autonomous_agent_api/backend/app/controllers/ready.py +++ b/autonomous_agent_api/backend/app/controllers/ready.py @@ -45,7 +45,9 @@ async def readiness_check() -> ReadyResponse: log.error("Could not connect to Redis") raise HTTPException( status_code=502, - content=ErrorResponse(code=502, message="Could not connect to Redis").dict(exclude_none=True), + content=ErrorResponse(code=502, message="Could not connect to Redis").dict( + exclude_none=True + ), ) return ReadyResponse(status="ok") diff --git a/autonomous_agent_api/backend/app/exceptions/http.py b/autonomous_agent_api/backend/app/exceptions/http.py index 48a6aa86c..ad05f9c82 100644 --- a/autonomous_agent_api/backend/app/exceptions/http.py +++ b/autonomous_agent_api/backend/app/exceptions/http.py @@ -53,7 +53,9 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({', '.join(kwargs)})" -async def http_exception_handler(request: Request, exception: HTTPException) -> JSONResponse: +async def http_exception_handler( + request: Request, exception: HTTPException +) -> JSONResponse: """Define custom HTTPException handler. In this application custom handler is added in asgi.py while initializing diff --git a/autonomous_agent_api/backend/app/repositories/agent_repository.py b/autonomous_agent_api/backend/app/repositories/agent_repository.py index aff46bc1a..04b7fd6e0 100644 --- a/autonomous_agent_api/backend/app/repositories/agent_repository.py +++ b/autonomous_agent_api/backend/app/repositories/agent_repository.py @@ -25,35 +25,51 @@ async def save_agent(self, agent_data: AgentCreateDTO): async def retrieve_agents(self) -> List[AgentResponse]: async with prisma_connection: - agents = await prisma_connection.prisma.agent.find_many(where={"deleted_at": None}) + agents = await prisma_connection.prisma.agent.find_many( + where={"deleted_at": None} + ) return agents async def retrieve_agent(self, agent_id: str) -> AgentResponse: async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id, "deleted_at": None}) + agent = await prisma_connection.prisma.agent.find_first( + where={"id": agent_id, "deleted_at": None} + ) if agent is None: raise HTTPException(status_code=404, detail="Agent not found") else: return agent - async def modify_agent(self, agent_id: str, agent_data: AgentCreateDTO) -> AgentResponse: + async def modify_agent( + self, agent_id: str, agent_data: AgentCreateDTO + ) -> AgentResponse: async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id}) + agent = await prisma_connection.prisma.agent.find_first( + where={"id": agent_id} + ) if agent is None or agent.deleted_at is not None: raise HTTPException(status_code=404, detail="Agent not found") updated_data = agent_data.dict(exclude_unset=True) updated_data["updated_at"] = datetime.utcnow() - updated_agent = await prisma_connection.prisma.agent.update(where={"id": agent_id}, data=updated_data) + updated_agent = await prisma_connection.prisma.agent.update( + where={"id": agent_id}, data=updated_data + ) return updated_agent async def remove_agent(self, agent_id: str) -> None: async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id}) + agent = await prisma_connection.prisma.agent.find_first( + where={"id": agent_id} + ) if agent is None: raise HTTPException(status_code=404, detail="Agent not found") elif agent.deleted_at is not None: - raise HTTPException(status_code=404, detail="Agent has already been deleted") + raise HTTPException( + status_code=404, detail="Agent has already been deleted" + ) - await prisma_connection.prisma.agent.update(where={"id": agent_id}, data={"deleted_at": datetime.now(UTC)}) + await prisma_connection.prisma.agent.update( + where={"id": agent_id}, data={"deleted_at": datetime.now(UTC)} + ) diff --git a/autonomous_agent_api/backend/app/services/agent_service.py b/autonomous_agent_api/backend/app/services/agent_service.py index d0c86c96b..f778d047b 100644 --- a/autonomous_agent_api/backend/app/services/agent_service.py +++ b/autonomous_agent_api/backend/app/services/agent_service.py @@ -19,7 +19,9 @@ async def list_agents(self) -> List[AgentResponse]: async def get_agent(self, agent_id: str) -> AgentResponse: return await self.agent_repository.retrieve_agent(agent_id) - async def update_agent(self, agent_id: str, agent_data: AgentCreateDTO) -> AgentResponse: + async def update_agent( + self, agent_id: str, agent_data: AgentCreateDTO + ) -> AgentResponse: return await self.agent_repository.modify_agent(agent_id, agent_data) async def delete_agent(self, agent_id: str) -> None: diff --git a/autonomous_agent_api/backend/app/utils/redis.py b/autonomous_agent_api/backend/app/utils/redis.py index 7a7b80cfa..5a607724c 100644 --- a/autonomous_agent_api/backend/app/utils/redis.py +++ b/autonomous_agent_api/backend/app/utils/redis.py @@ -245,7 +245,9 @@ async def lrange(cls, key: str, start: int, end: int) -> str: """ redis_client = cls.redis_client - cls.log.debug(f"Execute Redis LRANGE command, key: {key}, start: {start}, end: {end}") + cls.log.debug( + f"Execute Redis LRANGE command, key: {key}, start: {start}, end: {end}" + ) try: return await redis_client.lrange(key, start, end) except aioredis.RedisError as ex: diff --git a/autonomous_agent_api/backend/app/views/error.py b/autonomous_agent_api/backend/app/views/error.py index ea86b8874..19926b0b5 100644 --- a/autonomous_agent_api/backend/app/views/error.py +++ b/autonomous_agent_api/backend/app/views/error.py @@ -65,7 +65,9 @@ def schema_extra(schema: Dict[str, Any]) -> None: # Override schema description, by default is taken from docstring. schema["description"] = "Error model." # Add status to schema properties. - schema["properties"].update({"status": {"title": "Status", "type": "string"}}) + schema["properties"].update( + {"status": {"title": "Status", "type": "string"}} + ) schema["required"].append("status") diff --git a/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py b/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py index d481412fc..a039bed6d 100644 --- a/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py +++ b/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py @@ -38,7 +38,9 @@ async def test_should_close_aiohttp_client(self): (500, {"API": "KEY"}, False), ], ) - async def test_should_execute_get_and_return_response(self, fake_web, status, headers, raise_for_status): + async def test_should_execute_get_and_return_response( + self, fake_web, status, headers, raise_for_status + ): # given fake_web.get( "http://example.com/api", @@ -88,7 +90,9 @@ async def test_should_execute_get_and_raise(self, fake_web, status): (500, None, {"API": "KEY"}, False), ], ) - async def test_should_execute_post_and_return_response(self, fake_web, status, data, headers, raise_for_status): + async def test_should_execute_post_and_return_response( + self, fake_web, status, data, headers, raise_for_status + ): # given fake_web.post( "http://example.com/api", @@ -138,7 +142,9 @@ async def test_should_execute_post_and_raise(self, fake_web, status): (500, None, {"API": "KEY"}, False), ], ) - async def test_should_execute_put_and_return_response(self, fake_web, status, data, headers, raise_for_status): + async def test_should_execute_put_and_return_response( + self, fake_web, status, data, headers, raise_for_status + ): # given fake_web.put( "http://example.com/api", @@ -188,7 +194,9 @@ async def test_should_execute_put_and_raise(self, fake_web, status): (500, None, {"API": "KEY"}, False), ], ) - async def test_should_execute_patch_and_return_response(self, fake_web, status, data, headers, raise_for_status): + async def test_should_execute_patch_and_return_response( + self, fake_web, status, data, headers, raise_for_status + ): # given fake_web.patch( "http://example.com/api", @@ -238,7 +246,9 @@ async def test_should_execute_patch_and_raise(self, fake_web, status): (500, {"API": "KEY"}, False), ], ) - async def test_should_execute_delete_and_return_response(self, fake_web, status, headers, raise_for_status): + async def test_should_execute_delete_and_return_response( + self, fake_web, status, headers, raise_for_status + ): # given fake_web.delete( "http://example.com/api", diff --git a/autonomous_agent_api/tests/unit/cli/test_cli.py b/autonomous_agent_api/tests/unit/cli/test_cli.py index 2052066d6..ab86f6545 100644 --- a/autonomous_agent_api/tests/unit/cli/test_cli.py +++ b/autonomous_agent_api/tests/unit/cli/test_cli.py @@ -24,7 +24,9 @@ def test_should_exit_error_when_invoked_with_invalid_option(self, cli_runner): # then assert result.exit_code == 2 - @pytest.mark.parametrize("args", [["serve", "--help"], ["--verbose", "serve", "--help"]]) + @pytest.mark.parametrize( + "args", [["serve", "--help"], ["--verbose", "serve", "--help"]] + ) def test_should_exit_zero_when_invoked_with_options(self, cli_runner, args): # given / when result = cli_runner.invoke(cli, args) diff --git a/autonomous_agent_api/tests/unit/cli/test_serve.py b/autonomous_agent_api/tests/unit/cli/test_serve.py index 4331b2b79..3f032bdbd 100644 --- a/autonomous_agent_api/tests/unit/cli/test_serve.py +++ b/autonomous_agent_api/tests/unit/cli/test_serve.py @@ -105,7 +105,9 @@ def test_should_exit_error_when_invoked_with_invalid_option(self, cli_runner): ), ], ) - def test_should_create_wsgi_app_with_parsed_arguments(self, cli_runner, patched_serve, args, expected): + def test_should_create_wsgi_app_with_parsed_arguments( + self, cli_runner, patched_serve, args, expected + ): # given / when result = cli_runner.invoke(patched_serve, args) From 22f214b09631e4e515ea647072f6b51547dcaa76 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 13:38:48 +0545 Subject: [PATCH 08/20] rerun black lint inside backend --- autonomous_agent_api/backend/app/asgi.py | 8 ++--- .../backend/app/controllers/agent_router.py | 8 ++--- .../backend/app/controllers/ready.py | 4 +-- .../backend/app/exceptions/http.py | 4 +-- .../app/repositories/agent_repository.py | 32 +++++-------------- .../backend/app/services/agent_service.py | 4 +-- .../backend/app/utils/redis.py | 4 +-- .../backend/app/views/error.py | 4 +-- .../unit/app/utils/test_aiohttp_client.py | 20 +++--------- .../tests/unit/cli/test_cli.py | 4 +-- .../tests/unit/cli/test_serve.py | 4 +-- 11 files changed, 24 insertions(+), 72 deletions(-) diff --git a/autonomous_agent_api/backend/app/asgi.py b/autonomous_agent_api/backend/app/asgi.py index 8107be911..f60daea44 100644 --- a/autonomous_agent_api/backend/app/asgi.py +++ b/autonomous_agent_api/backend/app/asgi.py @@ -27,9 +27,7 @@ async def lifespan(app: FastAPI): logger.info("Starting Server") - if not os.environ.get( - "TESTING" - ): # check if its a testing enironment , if not then connect to databse + if not os.environ.get("TESTING"): # check if its a testing enironment , if not then connect to databse await prisma_connection.connect() yield @@ -40,9 +38,7 @@ async def lifespan(app: FastAPI): await AiohttpClient.close_aiohttp_client() - if not os.environ.get( - "TESTING" - ): # check if it's not a testing env , if not close databse connection + if not os.environ.get("TESTING"): # check if it's not a testing env , if not close databse connection await prisma_connection.disconnect() logger.info("Stopping Server") diff --git a/autonomous_agent_api/backend/app/controllers/agent_router.py b/autonomous_agent_api/backend/app/controllers/agent_router.py index 15a8e7aae..c6b38dcbf 100644 --- a/autonomous_agent_api/backend/app/controllers/agent_router.py +++ b/autonomous_agent_api/backend/app/controllers/agent_router.py @@ -11,9 +11,7 @@ class AgentRouter(Routable): - def __init__( - self, agent_service: AgentService = get_agent_service(), *args, **kwargs - ): + def __init__(self, agent_service: AgentService = get_agent_service(), *args, **kwargs): super().__init__(*args, **kwargs) self.agent_service = agent_service @@ -23,9 +21,7 @@ async def create_agent(self, agent_data: AgentCreateDTO): agent = await self.agent_service.create_agent(agent_data) return agent except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) - ) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) @get("/agents/", response_model=List[AgentResponse]) async def list_agents(self): diff --git a/autonomous_agent_api/backend/app/controllers/ready.py b/autonomous_agent_api/backend/app/controllers/ready.py index 7c1b79216..17bbf8928 100644 --- a/autonomous_agent_api/backend/app/controllers/ready.py +++ b/autonomous_agent_api/backend/app/controllers/ready.py @@ -45,9 +45,7 @@ async def readiness_check() -> ReadyResponse: log.error("Could not connect to Redis") raise HTTPException( status_code=502, - content=ErrorResponse(code=502, message="Could not connect to Redis").dict( - exclude_none=True - ), + content=ErrorResponse(code=502, message="Could not connect to Redis").dict(exclude_none=True), ) return ReadyResponse(status="ok") diff --git a/autonomous_agent_api/backend/app/exceptions/http.py b/autonomous_agent_api/backend/app/exceptions/http.py index ad05f9c82..48a6aa86c 100644 --- a/autonomous_agent_api/backend/app/exceptions/http.py +++ b/autonomous_agent_api/backend/app/exceptions/http.py @@ -53,9 +53,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({', '.join(kwargs)})" -async def http_exception_handler( - request: Request, exception: HTTPException -) -> JSONResponse: +async def http_exception_handler(request: Request, exception: HTTPException) -> JSONResponse: """Define custom HTTPException handler. In this application custom handler is added in asgi.py while initializing diff --git a/autonomous_agent_api/backend/app/repositories/agent_repository.py b/autonomous_agent_api/backend/app/repositories/agent_repository.py index 04b7fd6e0..aff46bc1a 100644 --- a/autonomous_agent_api/backend/app/repositories/agent_repository.py +++ b/autonomous_agent_api/backend/app/repositories/agent_repository.py @@ -25,51 +25,35 @@ async def save_agent(self, agent_data: AgentCreateDTO): async def retrieve_agents(self) -> List[AgentResponse]: async with prisma_connection: - agents = await prisma_connection.prisma.agent.find_many( - where={"deleted_at": None} - ) + agents = await prisma_connection.prisma.agent.find_many(where={"deleted_at": None}) return agents async def retrieve_agent(self, agent_id: str) -> AgentResponse: async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first( - where={"id": agent_id, "deleted_at": None} - ) + agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id, "deleted_at": None}) if agent is None: raise HTTPException(status_code=404, detail="Agent not found") else: return agent - async def modify_agent( - self, agent_id: str, agent_data: AgentCreateDTO - ) -> AgentResponse: + async def modify_agent(self, agent_id: str, agent_data: AgentCreateDTO) -> AgentResponse: async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first( - where={"id": agent_id} - ) + agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id}) if agent is None or agent.deleted_at is not None: raise HTTPException(status_code=404, detail="Agent not found") updated_data = agent_data.dict(exclude_unset=True) updated_data["updated_at"] = datetime.utcnow() - updated_agent = await prisma_connection.prisma.agent.update( - where={"id": agent_id}, data=updated_data - ) + updated_agent = await prisma_connection.prisma.agent.update(where={"id": agent_id}, data=updated_data) return updated_agent async def remove_agent(self, agent_id: str) -> None: async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first( - where={"id": agent_id} - ) + agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id}) if agent is None: raise HTTPException(status_code=404, detail="Agent not found") elif agent.deleted_at is not None: - raise HTTPException( - status_code=404, detail="Agent has already been deleted" - ) + raise HTTPException(status_code=404, detail="Agent has already been deleted") - await prisma_connection.prisma.agent.update( - where={"id": agent_id}, data={"deleted_at": datetime.now(UTC)} - ) + await prisma_connection.prisma.agent.update(where={"id": agent_id}, data={"deleted_at": datetime.now(UTC)}) diff --git a/autonomous_agent_api/backend/app/services/agent_service.py b/autonomous_agent_api/backend/app/services/agent_service.py index f778d047b..d0c86c96b 100644 --- a/autonomous_agent_api/backend/app/services/agent_service.py +++ b/autonomous_agent_api/backend/app/services/agent_service.py @@ -19,9 +19,7 @@ async def list_agents(self) -> List[AgentResponse]: async def get_agent(self, agent_id: str) -> AgentResponse: return await self.agent_repository.retrieve_agent(agent_id) - async def update_agent( - self, agent_id: str, agent_data: AgentCreateDTO - ) -> AgentResponse: + async def update_agent(self, agent_id: str, agent_data: AgentCreateDTO) -> AgentResponse: return await self.agent_repository.modify_agent(agent_id, agent_data) async def delete_agent(self, agent_id: str) -> None: diff --git a/autonomous_agent_api/backend/app/utils/redis.py b/autonomous_agent_api/backend/app/utils/redis.py index 5a607724c..7a7b80cfa 100644 --- a/autonomous_agent_api/backend/app/utils/redis.py +++ b/autonomous_agent_api/backend/app/utils/redis.py @@ -245,9 +245,7 @@ async def lrange(cls, key: str, start: int, end: int) -> str: """ redis_client = cls.redis_client - cls.log.debug( - f"Execute Redis LRANGE command, key: {key}, start: {start}, end: {end}" - ) + cls.log.debug(f"Execute Redis LRANGE command, key: {key}, start: {start}, end: {end}") try: return await redis_client.lrange(key, start, end) except aioredis.RedisError as ex: diff --git a/autonomous_agent_api/backend/app/views/error.py b/autonomous_agent_api/backend/app/views/error.py index 19926b0b5..ea86b8874 100644 --- a/autonomous_agent_api/backend/app/views/error.py +++ b/autonomous_agent_api/backend/app/views/error.py @@ -65,9 +65,7 @@ def schema_extra(schema: Dict[str, Any]) -> None: # Override schema description, by default is taken from docstring. schema["description"] = "Error model." # Add status to schema properties. - schema["properties"].update( - {"status": {"title": "Status", "type": "string"}} - ) + schema["properties"].update({"status": {"title": "Status", "type": "string"}}) schema["required"].append("status") diff --git a/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py b/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py index a039bed6d..d481412fc 100644 --- a/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py +++ b/autonomous_agent_api/tests/unit/app/utils/test_aiohttp_client.py @@ -38,9 +38,7 @@ async def test_should_close_aiohttp_client(self): (500, {"API": "KEY"}, False), ], ) - async def test_should_execute_get_and_return_response( - self, fake_web, status, headers, raise_for_status - ): + async def test_should_execute_get_and_return_response(self, fake_web, status, headers, raise_for_status): # given fake_web.get( "http://example.com/api", @@ -90,9 +88,7 @@ async def test_should_execute_get_and_raise(self, fake_web, status): (500, None, {"API": "KEY"}, False), ], ) - async def test_should_execute_post_and_return_response( - self, fake_web, status, data, headers, raise_for_status - ): + async def test_should_execute_post_and_return_response(self, fake_web, status, data, headers, raise_for_status): # given fake_web.post( "http://example.com/api", @@ -142,9 +138,7 @@ async def test_should_execute_post_and_raise(self, fake_web, status): (500, None, {"API": "KEY"}, False), ], ) - async def test_should_execute_put_and_return_response( - self, fake_web, status, data, headers, raise_for_status - ): + async def test_should_execute_put_and_return_response(self, fake_web, status, data, headers, raise_for_status): # given fake_web.put( "http://example.com/api", @@ -194,9 +188,7 @@ async def test_should_execute_put_and_raise(self, fake_web, status): (500, None, {"API": "KEY"}, False), ], ) - async def test_should_execute_patch_and_return_response( - self, fake_web, status, data, headers, raise_for_status - ): + async def test_should_execute_patch_and_return_response(self, fake_web, status, data, headers, raise_for_status): # given fake_web.patch( "http://example.com/api", @@ -246,9 +238,7 @@ async def test_should_execute_patch_and_raise(self, fake_web, status): (500, {"API": "KEY"}, False), ], ) - async def test_should_execute_delete_and_return_response( - self, fake_web, status, headers, raise_for_status - ): + async def test_should_execute_delete_and_return_response(self, fake_web, status, headers, raise_for_status): # given fake_web.delete( "http://example.com/api", diff --git a/autonomous_agent_api/tests/unit/cli/test_cli.py b/autonomous_agent_api/tests/unit/cli/test_cli.py index ab86f6545..2052066d6 100644 --- a/autonomous_agent_api/tests/unit/cli/test_cli.py +++ b/autonomous_agent_api/tests/unit/cli/test_cli.py @@ -24,9 +24,7 @@ def test_should_exit_error_when_invoked_with_invalid_option(self, cli_runner): # then assert result.exit_code == 2 - @pytest.mark.parametrize( - "args", [["serve", "--help"], ["--verbose", "serve", "--help"]] - ) + @pytest.mark.parametrize("args", [["serve", "--help"], ["--verbose", "serve", "--help"]]) def test_should_exit_zero_when_invoked_with_options(self, cli_runner, args): # given / when result = cli_runner.invoke(cli, args) diff --git a/autonomous_agent_api/tests/unit/cli/test_serve.py b/autonomous_agent_api/tests/unit/cli/test_serve.py index 3f032bdbd..4331b2b79 100644 --- a/autonomous_agent_api/tests/unit/cli/test_serve.py +++ b/autonomous_agent_api/tests/unit/cli/test_serve.py @@ -105,9 +105,7 @@ def test_should_exit_error_when_invoked_with_invalid_option(self, cli_runner): ), ], ) - def test_should_create_wsgi_app_with_parsed_arguments( - self, cli_runner, patched_serve, args, expected - ): + def test_should_create_wsgi_app_with_parsed_arguments(self, cli_runner, patched_serve, args, expected): # given / when result = cli_runner.invoke(patched_serve, args) From 5fb9c6577239a85ef717c2d2759e7ce33e423c26 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Fri, 29 Mar 2024 14:19:45 +0545 Subject: [PATCH 09/20] remove auto gerneated app.log --- app.log | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 app.log diff --git a/app.log b/app.log deleted file mode 100644 index 7bc6b2d92..000000000 --- a/app.log +++ /dev/null @@ -1,21 +0,0 @@ -2024-03-29 12:45:59 - INFO - Setting up Test Environment -2024-03-29 12:46:00 - ERROR - Invalid value for workers: None -2024-03-29 12:46:00 - INFO - Starting Server -2024-03-29 12:46:10 - CRITICAL - Error Connecting to Database: Could not connect to the query engine -2024-03-29 12:46:10 - INFO - Starting Server -2024-03-29 12:46:24 - INFO - Setting up Test Environment -2024-03-29 12:46:24 - ERROR - Invalid value for workers: None -2024-03-29 12:46:24 - INFO - Starting Server -2024-03-29 12:46:43 - INFO - Setting up Test Environment -2024-03-29 12:54:00 - INFO - Setting up Test Environment -2024-03-29 12:54:00 - ERROR - Invalid value for workers: None -2024-03-29 12:54:00 - INFO - Starting Server -2024-03-29 12:54:07 - INFO - Setting up Test Environment -2024-03-29 12:54:25 - INFO - Setting up Test Environment -2024-03-29 12:54:25 - ERROR - Invalid value for workers: None -2024-03-29 12:54:25 - INFO - Starting Server -2024-03-29 12:54:32 - INFO - Setting up Test Environment -2024-03-29 12:59:42 - INFO - Setting up Test Environment -2024-03-29 12:59:42 - ERROR - Invalid value for workers: None -2024-03-29 12:59:42 - INFO - Starting Server -2024-03-29 12:59:52 - INFO - Setting up Test Environment From 77cc7268b0a1362c503fc6303db322b03bd4a49f Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Mon, 1 Apr 2024 11:45:40 +0545 Subject: [PATCH 10/20] split backend workflow to build backend image and test backend --- .github/workflows/build-backend-image.yml | 28 +++++++++++++++++++ .../{run-backend.yml => test-backend.yml} | 23 ++------------- 2 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/build-backend-image.yml rename .github/workflows/{run-backend.yml => test-backend.yml} (59%) diff --git a/.github/workflows/build-backend-image.yml b/.github/workflows/build-backend-image.yml new file mode 100644 index 000000000..5aa711913 --- /dev/null +++ b/.github/workflows/build-backend-image.yml @@ -0,0 +1,28 @@ +name: Backend Service Docker Image Builder + +on: + push: + branches: ["dev"] + pull_request: + branches: ["dev"] + +jobs: + build_on_pr: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code with submodules + uses: actions/checkout@v2 + with: + submodules: "recursive" + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + push: false + tags: backend:latest + context: autonomous_agent_api diff --git a/.github/workflows/run-backend.yml b/.github/workflows/test-backend.yml similarity index 59% rename from .github/workflows/run-backend.yml rename to .github/workflows/test-backend.yml index cbcd780b1..d517734c3 100644 --- a/.github/workflows/run-backend.yml +++ b/.github/workflows/test-backend.yml @@ -1,5 +1,5 @@ -name: Basic Workflow test / Docker Image Builder +name: Backend Service Testing ( Pytest ) on: push: @@ -38,23 +38,4 @@ jobs: DATABASE_URL : "" run: poetry run pytest -m github_actions - build_on_pr: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes : 30 - steps: - - name: Checkout code with submodules - uses: actions/checkout@v2 - with: - submodules: "recursive" - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 - - - - name: Build Docker image - uses: docker/build-push-action@v5 - with: - push: false - tags: backend:latest - context: autonomous_agent_api + \ No newline at end of file From b1d6e57ad96a70f2fd3b2e0e4291e89586873424 Mon Sep 17 00:00:00 2001 From: roshan Date: Mon, 1 Apr 2024 11:46:21 +0545 Subject: [PATCH 11/20] wrote_test_cases_for_the_agents_api --- .../app/repositories/agent_repository.py | 50 ++++++++--------- autonomous_agent_api/poetry.lock | 17 +++++- .../tests/unit/app/conftest.py | 3 ++ .../controllers/agent_router_test/__init__.py | 0 .../agent_router_test/test_agent_delete.py | 33 ++++++++++++ .../agent_router_test/test_agent_list.py | 50 +++++++++++++++++ .../agent_router_test/test_create_agent.py | 50 +++++++++++++++++ .../agent_router_test/test_editing_agent.py | 53 +++++++++++++++++++ .../unit/app/controllers/test_create_agent.py | 22 -------- 9 files changed, 230 insertions(+), 48 deletions(-) create mode 100644 autonomous_agent_api/tests/unit/app/controllers/agent_router_test/__init__.py create mode 100644 autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_delete.py create mode 100644 autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py create mode 100644 autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py create mode 100644 autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py delete mode 100644 autonomous_agent_api/tests/unit/app/controllers/test_create_agent.py diff --git a/autonomous_agent_api/backend/app/repositories/agent_repository.py b/autonomous_agent_api/backend/app/repositories/agent_repository.py index aff46bc1a..bbfeec0d6 100644 --- a/autonomous_agent_api/backend/app/repositories/agent_repository.py +++ b/autonomous_agent_api/backend/app/repositories/agent_repository.py @@ -1,59 +1,59 @@ import uuid -from datetime import datetime, UTC - - -from typing import List - +from datetime import datetime, timezone +from typing import List, Optional from fastapi import HTTPException - from backend.app.models.agent_dto import AgentCreateDTO from backend.app.models.response_dto import AgentResponse from backend.config.database import prisma_connection class AgentRepository: + def __init__(self, db_connection=None): + self.db = db_connection or prisma_connection + async def save_agent(self, agent_data: AgentCreateDTO): # Generate a random UUID for the agent ID agent_id = str(uuid.uuid4()) agent_data_dict = agent_data.dict() agent_data_dict["id"] = agent_id - agent_data_dict["created_at"] = datetime.now(UTC) + agent_data_dict["created_at"] = datetime.now(timezone.utc) - async with prisma_connection: - agent = await prisma_connection.prisma.agent.create(data=agent_data_dict) + async with self.db: + agent = await self.db.prisma.agent.create(data=agent_data_dict) return agent async def retrieve_agents(self) -> List[AgentResponse]: - async with prisma_connection: - agents = await prisma_connection.prisma.agent.find_many(where={"deleted_at": None}) + async with self.db: + agents = await self.db.prisma.agent.find_many(where={"deleted_at": None}) return agents - async def retrieve_agent(self, agent_id: str) -> AgentResponse: - async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id, "deleted_at": None}) + async def retrieve_agent(self, agent_id: str) -> Optional[AgentResponse]: + async with self.db: + agent = await self.db.prisma.agent.find_first(where={"id": agent_id, "deleted_at": None}) if agent is None: raise HTTPException(status_code=404, detail="Agent not found") else: return agent - async def modify_agent(self, agent_id: str, agent_data: AgentCreateDTO) -> AgentResponse: - async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id}) + async def modify_agent(self, agent_id: str, agent_data: AgentCreateDTO) -> Optional[AgentResponse]: + async with self.db: + agent = await self.db.prisma.agent.find_first(where={"id": agent_id}) if agent is None or agent.deleted_at is not None: raise HTTPException(status_code=404, detail="Agent not found") updated_data = agent_data.dict(exclude_unset=True) - updated_data["updated_at"] = datetime.utcnow() + updated_data["updated_at"] = datetime.now(timezone.utc) - updated_agent = await prisma_connection.prisma.agent.update(where={"id": agent_id}, data=updated_data) + updated_agent = await self.db.prisma.agent.update(where={"id": agent_id}, data=updated_data) return updated_agent - async def remove_agent(self, agent_id: str) -> None: - async with prisma_connection: - agent = await prisma_connection.prisma.agent.find_first(where={"id": agent_id}) + async def remove_agent(self, agent_id: str) -> bool: + async with self.db: + agent = await self.db.prisma.agent.find_first(where={"id": agent_id}) if agent is None: - raise HTTPException(status_code=404, detail="Agent not found") + return False elif agent.deleted_at is not None: - raise HTTPException(status_code=404, detail="Agent has already been deleted") + return True - await prisma_connection.prisma.agent.update(where={"id": agent_id}, data={"deleted_at": datetime.now(UTC)}) + await self.db.prisma.agent.update(where={"id": agent_id}, data={"deleted_at": datetime.now(timezone.utc)}) + return True diff --git a/autonomous_agent_api/poetry.lock b/autonomous_agent_api/poetry.lock index 27389cf91..d9b0aac64 100644 --- a/autonomous_agent_api/poetry.lock +++ b/autonomous_agent_api/poetry.lock @@ -363,6 +363,21 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "classy-fastapi" +version = "0.6.1" +description = "Class based routing for FastAPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "classy-fastapi-0.6.1.tar.gz", hash = "sha256:5dfc33bab8e01e07c56855b78ce9a8152c871ab544a565d0d3d05a5c1ca4ed68"}, + {file = "classy_fastapi-0.6.1-py3-none-any.whl", hash = "sha256:196e5c2890269627d52851f3f86001a0dfda0070053d38f8a7bd896ac2f67737"}, +] + +[package.dependencies] +fastapi = ">=0.73.0,<1.0.0" +pydantic = ">=1.10.2,<3.0.0" + [[package]] name = "click" version = "8.1.7" @@ -2432,4 +2447,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ee6533f48aa232f19dc21dab76dce4b59f0b52c4097884fd647b18a0ae502807" +content-hash = "5fc1a77c6954037702b82110f3c48616bbe266ccc674a36e7df6a1232c671c45" diff --git a/autonomous_agent_api/tests/unit/app/conftest.py b/autonomous_agent_api/tests/unit/app/conftest.py index dcc958e46..f099f989e 100644 --- a/autonomous_agent_api/tests/unit/app/conftest.py +++ b/autonomous_agent_api/tests/unit/app/conftest.py @@ -1,7 +1,10 @@ +from unittest.mock import MagicMock + import pytest from fastapi.testclient import TestClient from backend.app import get_application from backend.config import settings +from backend.config.database import PrismaConnection @pytest.fixture diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/__init__.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_delete.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_delete.py new file mode 100644 index 000000000..77ce4d251 --- /dev/null +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_delete.py @@ -0,0 +1,33 @@ +import pytest +from unittest.mock import MagicMock, AsyncMock +from http import HTTPStatus +from backend.app.controllers.agent_router import AgentRouter +from fastapi import HTTPException +from fastapi import status + + +class TestAgentDelete: + @pytest.fixture + def agent_service(self): + return AsyncMock() + + @pytest.fixture + def agent_router(self, agent_service): + return AgentRouter(agent_service) + + @pytest.mark.asyncio + async def test_delete_agent_with_valid_id(self, agent_service, agent_router): + # Mock data + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + + await agent_router.delete_agent(agent_id) + + agent_service.delete_agent.assert_called_once_with(agent_id) + + @pytest.mark.asyncio + async def test_delete_agent_should_fail_with_no_id(self, agent_service, agent_router): + # Mock data + with pytest.raises(TypeError): + await agent_router.delete_agent() + + agent_service.delete_agent.assert_called_once_with() diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py new file mode 100644 index 000000000..8636d01f2 --- /dev/null +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py @@ -0,0 +1,50 @@ +from typing import List +from unittest.mock import MagicMock, AsyncMock + +from backend.app.controllers.agent_router import AgentRouter +from backend.app.models.response_dto import AgentResponse +from backend.app.services.agent_service import AgentService +from fastapi import HTTPException + +import pytest + + +@pytest.fixture +def agent_service(): + mock_service = MagicMock(spec=AgentService) + return mock_service + + +@pytest.fixture +def agent_router(agent_service): + return AgentRouter(agent_service) + + +# test for listing the agents +@pytest.mark.asyncio +async def test_list_agents_with_two_agents(agent_service, agent_router): + # Mocking the list_agents method to return a list of mock agents + mock_agents = [ + AgentResponse( + id="018e8908-5dc9-78c9-b71a-37ebd2149394", + name="Agent 1", + triggers=["Description 1"], + action=["Description 1"], + ), + AgentResponse( + id="018e8908-7563-7e8a-b87c-b33ac6e6c872", + name="Agent 2", + triggers=["Description 2"], + action=["Description 2"], + ), + ] + agent_service.list_agents = AsyncMock(return_value=mock_agents) + + # Call the list_agents method + agents = await agent_router.list_agents() + + # calling method + agent_service.list_agents.assert_called_once() + + # Assert that the returned agents match the mock_agents + assert agents == mock_agents diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py new file mode 100644 index 000000000..fd56eb7b3 --- /dev/null +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py @@ -0,0 +1,50 @@ +from pydantic import ValidationError +from backend.app.controllers.agent_router import AgentRouter +from backend.app.models.agent_dto import AgentCreateDTO +from backend.app.models.response_dto import AgentResponse +from backend.app.services.agent_service import AgentService +from unittest.mock import MagicMock, AsyncMock +import pytest + + +@pytest.mark.asyncio +class TestAgentCreateRouter: + @pytest.fixture + def agent_service(self): + return MagicMock(spec=AgentService) + + @pytest.fixture + def agent_router(self, agent_service): + return AgentRouter(agent_service) + + async def test_update_agent_with_valid_details(self, agent_service, agent_router): + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + agent_data = AgentCreateDTO(name="Updated Agent", triggers=["Test Description"], action=["Test Description"]) + updated_agent = AgentResponse( + id=agent_id, name="Updated Agent", triggers=["Test Description"], action=["Test Description"] + ) + + agent_service.update_agent = AsyncMock(return_value=updated_agent) + + result = await agent_router.update_agent(agent_id, agent_data) + + agent_service.update_agent.assert_called_once_with(agent_id, agent_data) + + assert result == updated_agent + + async def test_update_agent_should_fail_with_invalid_details(self, agent_service, agent_router): + with pytest.raises(ValidationError): + # Mock data + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + agent_data = AgentCreateDTO(name="", triggers=["Test Description"], action=["Test Description"]) + updated_agent = AgentResponse( + id=agent_id, name="Agent", triggers=["Test Description"], action=["Test Description"] + ) + + agent_service.update_agent = AsyncMock(return_value=updated_agent) + + result = await agent_router.update_agent(agent_id, agent_data) + + agent_service.update_agent.assert_called_once_with(agent_id, agent_data) + + assert result == updated_agent diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py new file mode 100644 index 000000000..9904030a3 --- /dev/null +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py @@ -0,0 +1,53 @@ +from pydantic import ValidationError + +from backend.app.controllers.agent_router import AgentRouter +from backend.app.models.agent_dto import AgentCreateDTO +from backend.app.models.response_dto import AgentResponse +from backend.app.services.agent_service import AgentService +from unittest.mock import MagicMock, AsyncMock +import pytest + + +@pytest.mark.asyncio +class TestAgentEditRouter: + @pytest.fixture + def agent_service(self): + return MagicMock(spec=AgentService) + + @pytest.fixture + def agent_router(self, agent_service): + return AgentRouter(agent_service) + + @pytest.mark.asyncio + async def test_update_agent_with_valid_details(self, agent_service, agent_router): + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + agent_data = AgentCreateDTO(name="Updated Agent", triggers=["Test Description"], action=["Test Description"]) + updated_agent = AgentResponse( + id=agent_id, name="Updated Agent", triggers=["Test Description"], action=["Test Description"] + ) + + agent_service.update_agent = AsyncMock(return_value=updated_agent) + + result = await agent_router.update_agent(agent_id, agent_data) + + agent_service.update_agent.assert_called_once_with(agent_id, agent_data) + + assert result == updated_agent + + @pytest.mark.asyncio + async def test_update_agent_should_fail_with_invalid_details(self, agent_service, agent_router): + with pytest.raises(ValidationError): + # Mock data + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + agent_data = AgentCreateDTO(name="", triggers=["Test Description"], action=["Test Description"]) + updated_agent = AgentResponse( + id=agent_id, name="Agent", triggers=["Test Description"], action=["Test Description"] + ) + + agent_service.update_agent = AsyncMock(return_value=updated_agent) + + result = await agent_router.update_agent(agent_id, agent_data) + + agent_service.update_agent.assert_called_once_with(agent_id, agent_data) + + assert result == updated_agent diff --git a/autonomous_agent_api/tests/unit/app/controllers/test_create_agent.py b/autonomous_agent_api/tests/unit/app/controllers/test_create_agent.py deleted file mode 100644 index 489995984..000000000 --- a/autonomous_agent_api/tests/unit/app/controllers/test_create_agent.py +++ /dev/null @@ -1,22 +0,0 @@ -from backend.app.asgi import get_application -from fastapi.testclient import TestClient -from fastapi import status -import pytest - - -client = TestClient(get_application()) - - -@pytest.mark.create_agent -def test_create_agent(): - agent_data = {"name": "Test Agent", "action": [], "triggers": []} - response = client.post("/create_agents/", json=agent_data) - - assert response.status_code == status.HTTP_200_OK - assert "id" in response.json() - assert response.json()["name"] == "Test Agent" - assert response.json()["action"] == [] - assert response.json()["triggers"] == [] - assert "created_at" in response.json() - assert "updated_at" is None - assert "deleted_at" is None From cd982f70ada1e312894328ff9f048a13d81d40eb Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Mon, 1 Apr 2024 12:40:22 +0545 Subject: [PATCH 12/20] added test lifespan --- autonomous_agent_api/backend/app/asgi.py | 29 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/autonomous_agent_api/backend/app/asgi.py b/autonomous_agent_api/backend/app/asgi.py index f60daea44..a5f223aba 100644 --- a/autonomous_agent_api/backend/app/asgi.py +++ b/autonomous_agent_api/backend/app/asgi.py @@ -27,8 +27,7 @@ async def lifespan(app: FastAPI): logger.info("Starting Server") - if not os.environ.get("TESTING"): # check if its a testing enironment , if not then connect to databse - await prisma_connection.connect() + await prisma_connection.connect() yield log.debug("Execute FastAPI shutdown event handler.") @@ -38,12 +37,29 @@ async def lifespan(app: FastAPI): await AiohttpClient.close_aiohttp_client() - if not os.environ.get("TESTING"): # check if it's not a testing env , if not close databse connection - await prisma_connection.disconnect() + await prisma_connection.disconnect() logger.info("Stopping Server") +# Lifespan used for Test Environmnet : Configurations such as Live Database and Redis is Disabled. +# todo : Mock Database setup required for testing +@asynccontextmanager +async def test_lifespan(app: FastAPI): + log.debug("Execute FastAPI startup event handler.") + if settings.USE_REDIS: + await RedisClient.open_redis_client() + + AiohttpClient.get_aiohttp_client() + + logger.info("Starting Test Server") + + yield + log.debug("Execute FastAPI shutdown event handler.") + + logger.info("Stopping Test Server") + + def get_application() -> FastAPI: """Initialize FastAPI application. @@ -52,6 +68,7 @@ def get_application() -> FastAPI: """ log.debug("Initialize FastAPI application node.") + app = FastAPI( title=settings.PROJECT_NAME, debug=settings.DEBUG, @@ -69,7 +86,7 @@ def get_application() -> FastAPI: def get_test_application() -> FastAPI: """ - Initialize FastApi application for testing + Initialize FastApi application for testing environment Returns: FastAPI : Application object instance @@ -83,7 +100,7 @@ def get_test_application() -> FastAPI: debug=settings.DEBUG, version=settings.VERSION, docs_url=settings.DOCS_URL, - lifespan=lifespan, + lifespan=test_lifespan, ) log.debug("Add application routes.") app.include_router(root_api_router) From 5dd8b4721dc174573bfe42e4307e431f2c152e66 Mon Sep 17 00:00:00 2001 From: Joseph Rana Date: Mon, 1 Apr 2024 12:45:11 +0545 Subject: [PATCH 13/20] remove redis client connection in test_lifespan --- autonomous_agent_api/backend/app/asgi.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/autonomous_agent_api/backend/app/asgi.py b/autonomous_agent_api/backend/app/asgi.py index a5f223aba..92b4e9754 100644 --- a/autonomous_agent_api/backend/app/asgi.py +++ b/autonomous_agent_api/backend/app/asgi.py @@ -47,8 +47,6 @@ async def lifespan(app: FastAPI): @asynccontextmanager async def test_lifespan(app: FastAPI): log.debug("Execute FastAPI startup event handler.") - if settings.USE_REDIS: - await RedisClient.open_redis_client() AiohttpClient.get_aiohttp_client() From fa3efc06a62a03129e1ed609ef95a2c586932e87 Mon Sep 17 00:00:00 2001 From: roshan Date: Tue, 2 Apr 2024 11:04:50 +0545 Subject: [PATCH 14/20] completed crud operation of adding trigger for agent --- .../backend/app/controllers/agent_router.py | 6 +- .../controllers/cron_trigger_config_router.py | 0 .../backend/app/controllers/trigger_router.py | 51 +++++++++ .../backend/app/models/AgentDto/__init__.py | 0 .../app/models/{ => AgentDto}/agent_dto.py | 1 - .../app/models/{ => AgentDto}/response_dto.py | 1 - .../backend/app/models/TriggerDto/__init__.py | 0 .../app/models/TriggerDto/resposne_dto.py | 17 +++ .../app/models/TriggerDto/trigger_dto.py | 22 ++++ .../app/repositories/agent_repository.py | 12 +- .../app/repositories/trigger_repository.py | 107 ++++++++++++++++++ autonomous_agent_api/backend/app/router.py | 4 +- .../backend/app/services/agent_service.py | 4 +- .../backend/app/services/trigger_service.py | 32 ++++++ autonomous_agent_api/backend/dependency.py | 8 ++ .../migrations/20240327040548_0/migration.sql | 15 --- .../migration.sql | 27 +++++ autonomous_agent_api/prisma/schema.prisma | 16 ++- .../agent_router_test/test_agent_list.py | 6 +- .../agent_router_test/test_create_agent.py | 16 +-- .../agent_router_test/test_editing_agent.py | 16 +-- 21 files changed, 308 insertions(+), 53 deletions(-) create mode 100644 autonomous_agent_api/backend/app/controllers/cron_trigger_config_router.py create mode 100644 autonomous_agent_api/backend/app/controllers/trigger_router.py create mode 100644 autonomous_agent_api/backend/app/models/AgentDto/__init__.py rename autonomous_agent_api/backend/app/models/{ => AgentDto}/agent_dto.py (89%) rename autonomous_agent_api/backend/app/models/{ => AgentDto}/response_dto.py (83%) create mode 100644 autonomous_agent_api/backend/app/models/TriggerDto/__init__.py create mode 100644 autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py create mode 100644 autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py create mode 100644 autonomous_agent_api/backend/app/repositories/trigger_repository.py create mode 100644 autonomous_agent_api/backend/app/services/trigger_service.py delete mode 100644 autonomous_agent_api/prisma/migrations/20240327040548_0/migration.sql create mode 100644 autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql diff --git a/autonomous_agent_api/backend/app/controllers/agent_router.py b/autonomous_agent_api/backend/app/controllers/agent_router.py index c6b38dcbf..e9efff44d 100644 --- a/autonomous_agent_api/backend/app/controllers/agent_router.py +++ b/autonomous_agent_api/backend/app/controllers/agent_router.py @@ -2,11 +2,11 @@ from typing import List from classy_fastapi import Routable, get, post, put, delete -from fastapi import Query, HTTPException +from fastapi import HTTPException -from backend.app.models.agent_dto import AgentCreateDTO +from backend.app.models.AgentDto.agent_dto import AgentCreateDTO from backend.app.services.agent_service import AgentService -from backend.app.models.response_dto import AgentResponse +from backend.app.models.AgentDto.response_dto import AgentResponse from backend.dependency import get_agent_service diff --git a/autonomous_agent_api/backend/app/controllers/cron_trigger_config_router.py b/autonomous_agent_api/backend/app/controllers/cron_trigger_config_router.py new file mode 100644 index 000000000..e69de29bb diff --git a/autonomous_agent_api/backend/app/controllers/trigger_router.py b/autonomous_agent_api/backend/app/controllers/trigger_router.py new file mode 100644 index 000000000..3956e08d0 --- /dev/null +++ b/autonomous_agent_api/backend/app/controllers/trigger_router.py @@ -0,0 +1,51 @@ +from http import HTTPStatus +from typing import List + +from classy_fastapi import Routable, get, post, put, delete +from fastapi import HTTPException + +from backend.app.models.TriggerDto.resposne_dto import TriggerResponse +from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerEditDTO +from backend.app.services.trigger_service import TriggerService +from backend.dependency import get_trigger_service + + +class TriggerRouter(Routable): + def __init__(self, trigger_service: TriggerService = get_trigger_service(), *args, **kwargs): + super().__init__(*args, **kwargs) + self.trigger_service = trigger_service + + @post("/create_trigger/", status_code=HTTPStatus.CREATED) + async def create_trigger(self, trigger_data: TriggerCreateDTO): + trigger = await self.trigger_service.create_trigger(trigger_data) + return trigger + + @get("/triggers", response_model=List[TriggerResponse]) + async def list_triggers(self): + triggers = await self.trigger_service.list_triggers() + return triggers + + @get("/listTriggersByAgentId/{agent_id}", response_model=List[TriggerResponse]) + async def list_triggers_by_agent_id(self, agent_id: str): + triggers = await self.trigger_service.list_triggers_by_agent_id(agent_id) + return triggers + + @get("/listTriggerByAgentIdAndTriggerId/{agent_id}/{trigger_id}", response_model=TriggerResponse) + async def list_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str): + trigger = await self.trigger_service.list_trigger_by_agent_id_and_trigger_id(agent_id, trigger_id) + return trigger + + @put("/updateTriggerByAgentIdAndTriggerId/{agent_id}/{trigger_id}", response_model=TriggerResponse) + async def update_trigger_by_agent_id_and_trigger_id( + self, agent_id: str, trigger_id: str, trigger_data: TriggerEditDTO + ): + trigger = await self.trigger_service.update_trigger_by_agent_id_and_trigger_id( + agent_id, trigger_id, trigger_data + ) + return trigger + + @delete("/DeleteTriggerByAgentIdAndTriggerId/{agent_id}/{trigger_id}", status_code=HTTPStatus.NO_CONTENT) + async def delete_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str): + success = await self.trigger_service.delete_trigger_by_agent_id_and_trigger_id(agent_id, trigger_id) + if not success: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Trigger not found") diff --git a/autonomous_agent_api/backend/app/models/AgentDto/__init__.py b/autonomous_agent_api/backend/app/models/AgentDto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/autonomous_agent_api/backend/app/models/agent_dto.py b/autonomous_agent_api/backend/app/models/AgentDto/agent_dto.py similarity index 89% rename from autonomous_agent_api/backend/app/models/agent_dto.py rename to autonomous_agent_api/backend/app/models/AgentDto/agent_dto.py index d2b91e5a3..462d63a41 100644 --- a/autonomous_agent_api/backend/app/models/agent_dto.py +++ b/autonomous_agent_api/backend/app/models/AgentDto/agent_dto.py @@ -7,4 +7,3 @@ class AgentCreateDTO(BaseModel): name: str = Field(..., description="Name of Agent", min_length=1) action: List[str] = [] - triggers: List[str] = [] diff --git a/autonomous_agent_api/backend/app/models/response_dto.py b/autonomous_agent_api/backend/app/models/AgentDto/response_dto.py similarity index 83% rename from autonomous_agent_api/backend/app/models/response_dto.py rename to autonomous_agent_api/backend/app/models/AgentDto/response_dto.py index 6fafc7d4e..06ca3978d 100644 --- a/autonomous_agent_api/backend/app/models/response_dto.py +++ b/autonomous_agent_api/backend/app/models/AgentDto/response_dto.py @@ -6,4 +6,3 @@ class AgentResponse(BaseModel): id: str name: str action: List[str] = [] - triggers: List[str] = [] diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/__init__.py b/autonomous_agent_api/backend/app/models/TriggerDto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py b/autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py new file mode 100644 index 000000000..bdaceffbf --- /dev/null +++ b/autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel +from typing import Union + +from backend.app.models.TriggerDto.trigger_dto import CronTriggerDTO, TopicTriggerDTO + + +class TriggerResponse(BaseModel): + id: str + agent_id: str + type: str + data: Union[CronTriggerDTO, TopicTriggerDTO] + + +# class TriggerResponse_agent_id(BaseModel): +# agentId: str +# type: str +# data: Union[CronTriggerDTO, TopicTriggerDTO] diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py b/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py new file mode 100644 index 000000000..a1cc0942e --- /dev/null +++ b/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py @@ -0,0 +1,22 @@ +from typing import Union +from pydantic import BaseModel, Json + + +class CronTriggerDTO(BaseModel): + frequency: str + probability: float + + +class TopicTriggerDTO(BaseModel): + topic: str + + +class TriggerCreateDTO(BaseModel): + type: str + data: Union[CronTriggerDTO, TopicTriggerDTO] + agent_id: str + + +class TriggerEditDTO(BaseModel): + type: str + data: Union[CronTriggerDTO, TopicTriggerDTO] diff --git a/autonomous_agent_api/backend/app/repositories/agent_repository.py b/autonomous_agent_api/backend/app/repositories/agent_repository.py index bbfeec0d6..e37328bf3 100644 --- a/autonomous_agent_api/backend/app/repositories/agent_repository.py +++ b/autonomous_agent_api/backend/app/repositories/agent_repository.py @@ -2,8 +2,8 @@ from datetime import datetime, timezone from typing import List, Optional from fastapi import HTTPException -from backend.app.models.agent_dto import AgentCreateDTO -from backend.app.models.response_dto import AgentResponse +from backend.app.models.AgentDto.agent_dto import AgentCreateDTO +from backend.app.models.AgentDto.response_dto import AgentResponse from backend.config.database import prisma_connection @@ -20,7 +20,9 @@ async def save_agent(self, agent_data: AgentCreateDTO): async with self.db: agent = await self.db.prisma.agent.create(data=agent_data_dict) - return agent + agent_response = AgentResponse(id=agent_id, name=agent_data.name, action=agent_data.action) + + return agent_response async def retrieve_agents(self) -> List[AgentResponse]: async with self.db: @@ -55,5 +57,7 @@ async def remove_agent(self, agent_id: str) -> bool: elif agent.deleted_at is not None: return True - await self.db.prisma.agent.update(where={"id": agent_id}, data={"deleted_at": datetime.now(timezone.utc)}) + await self.db.prisma.agstringent.update( + where={"id": agent_id}, data={"deleted_at": datetime.now(timezone.utc)} + ) return True diff --git a/autonomous_agent_api/backend/app/repositories/trigger_repository.py b/autonomous_agent_api/backend/app/repositories/trigger_repository.py new file mode 100644 index 000000000..ea6dd38ef --- /dev/null +++ b/autonomous_agent_api/backend/app/repositories/trigger_repository.py @@ -0,0 +1,107 @@ +import json +import uuid +from datetime import datetime, timezone +from typing import List, Optional, Union +from fastapi import HTTPException + +from backend.app.models.TriggerDto.resposne_dto import TriggerResponse +from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerEditDTO, CronTriggerDTO, TopicTriggerDTO +from backend.config.database import prisma_connection + + +class TriggerRepository: + def __init__(self, db_connection=None): + self.db = db_connection or prisma_connection + + async def save_trigger(self, trigger_data: TriggerCreateDTO): + trigger_id = str(uuid.uuid4()) + trigger_data_dict = trigger_data.dict() + + # Extract the data field and convert it to JSON + data_dict = trigger_data_dict.pop("data") + data_json = json.dumps(data_dict) + + # Assign the JSON data back to the dictionary + trigger_data_dict["id"] = trigger_id + trigger_data_dict["data"] = data_json + + trigger_data_dict["created_at"] = datetime.now(timezone.utc) + + async with self.db: + await self.db.prisma.trigger.create(data=trigger_data_dict) + + # Convert the JSON data back to the appropriate DTO object + data_object = self._convert_data_to_dto(trigger_data.type, data_dict) + + # Create a TriggerResponse object with the converted data + trigger_response = TriggerResponse( + id=trigger_id, agent_id=trigger_data.agent_id, type=trigger_data.type, data=data_object + ) + return trigger_response + + async def retreive_triggers(self) -> List[TriggerResponse]: + async with self.db: + triggers = await self.db.prisma.trigger.find_many() + return triggers + + async def retreive_triggers_by_agent_id(self, agent_id: str) -> List[TriggerResponse]: + async with self.db: + triggers = await self.db.prisma.trigger.find_many(where={"agent_id": agent_id, "deleted_at": None}) + return triggers + + async def remove_trigger_by_agent_id_trigger_id(self, agent_id: str, trigger_id: str) -> bool: + async with self.db: + trigger = await self.db.prisma.trigger.find_first(where={"agent_id": agent_id, "id": trigger_id}) + if trigger is None: + return False + elif trigger.deleted_at is not None: + return True + + await self.db.prisma.trigger.update( + where={"agent_id": agent_id, "id": trigger_id}, data={"deleted_at": datetime.now(timezone.utc)} + ) + return True + + async def retreive_trigger_by_agent_id_and_trigger_id( + self, agent_id: str, trigger_id: str + ) -> Optional[TriggerResponse]: + async with self.db: + trigger = await self.db.prisma.trigger.find_first( + where={"agent_id": agent_id, "id": trigger_id, "deleted_at": None} + ) + if trigger is None: + raise HTTPException(status_code=404, detail="Trigger not found") + else: + return trigger + + async def modify_trigger_by_agent_id_and_trigger_id( + self, agent_id: str, trigger_id: str, trigger_data: TriggerEditDTO + ) -> Optional[TriggerResponse]: + async with self.db: + trigger = await self.db.prisma.trigger.find_first(where={"agent_id": agent_id, "id": trigger_id}) + if trigger is None or trigger.deleted_at is not None: + raise HTTPException(status_code=404, detail="Trigger not found") + updated_data_dict = trigger_data.dict() + data_dict = updated_data_dict.pop("data") + data_json = json.dumps(data_dict) + + updated_data_dict["data"] = data_json + updated_data_dict["updated_at"] = datetime.now(timezone.utc) + + updated_trigger = await self.db.prisma.trigger.update( + where={"agent_id": agent_id, "id": trigger_id}, data=updated_data_dict + ) + + # Create a TriggerResponse object with the converted data + trigger_response = TriggerResponse( + id=trigger_id, agent_id=agent_id, type=trigger_data.type, data=trigger_data.data + ) + return trigger_response + + def _convert_data_to_dto(self, trigger_type: str, data_dict: dict) -> Union[CronTriggerDTO, TopicTriggerDTO]: + if trigger_type == "CRON": + return CronTriggerDTO(**data_dict) + elif trigger_type == "TOPIC": + return TopicTriggerDTO(**data_dict) + else: + raise ValueError("Invalid trigger type") diff --git a/autonomous_agent_api/backend/app/router.py b/autonomous_agent_api/backend/app/router.py index de93ca5ba..d3eee69b3 100644 --- a/autonomous_agent_api/backend/app/router.py +++ b/autonomous_agent_api/backend/app/router.py @@ -7,7 +7,7 @@ """ from fastapi import APIRouter -from backend.app.controllers import ready, demo, agent_router +from backend.app.controllers import ready, demo, agent_router, trigger_router root_api_router = APIRouter(prefix="/api") @@ -16,3 +16,5 @@ root_api_router.include_router(demo.router, tags=["test"]) root_api_router.include_router(agent_router.AgentRouter().router, tags=["agent"]) + +root_api_router.include_router(trigger_router.TriggerRouter().router, tags=["trigger"]) diff --git a/autonomous_agent_api/backend/app/services/agent_service.py b/autonomous_agent_api/backend/app/services/agent_service.py index d0c86c96b..1462db1df 100644 --- a/autonomous_agent_api/backend/app/services/agent_service.py +++ b/autonomous_agent_api/backend/app/services/agent_service.py @@ -1,8 +1,8 @@ # agent_service.py from typing import List -from backend.app.models.agent_dto import AgentCreateDTO -from backend.app.models.response_dto import AgentResponse +from backend.app.models.AgentDto.agent_dto import AgentCreateDTO +from backend.app.models.AgentDto.response_dto import AgentResponse from backend.app.repositories.agent_repository import AgentRepository diff --git a/autonomous_agent_api/backend/app/services/trigger_service.py b/autonomous_agent_api/backend/app/services/trigger_service.py new file mode 100644 index 000000000..c8f49e57e --- /dev/null +++ b/autonomous_agent_api/backend/app/services/trigger_service.py @@ -0,0 +1,32 @@ +from typing import List + +from backend.app.models.TriggerDto.resposne_dto import TriggerResponse +from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerEditDTO +from backend.app.repositories.trigger_repository import TriggerRepository + + +class TriggerService: + def __init__(self, trigger_repository: TriggerRepository): + self.trigger_repository = trigger_repository + + async def create_trigger(self, trigger_data: TriggerCreateDTO) -> TriggerResponse: + return await self.trigger_repository.save_trigger(trigger_data) + + async def list_triggers(self) -> List[TriggerResponse]: + return await self.trigger_repository.retreive_triggers() + + async def list_triggers_by_agent_id(self, agent_id: str) -> List[TriggerResponse]: + return await self.trigger_repository.retreive_triggers_by_agent_id(agent_id) + + async def delete_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str) -> bool: + return await self.trigger_repository.remove_trigger_by_agent_id_trigger_id(agent_id, trigger_id) + + async def list_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str) -> TriggerResponse: + return await self.trigger_repository.retreive_trigger_by_agent_id_and_trigger_id(agent_id, trigger_id) + + async def update_trigger_by_agent_id_and_trigger_id( + self, agent_id: str, trigger_id: str, trigger_data: TriggerEditDTO + ) -> TriggerResponse: + return await self.trigger_repository.modify_trigger_by_agent_id_and_trigger_id( + agent_id, trigger_id, trigger_data + ) diff --git a/autonomous_agent_api/backend/dependency.py b/autonomous_agent_api/backend/dependency.py index 284469a34..7191fe969 100644 --- a/autonomous_agent_api/backend/dependency.py +++ b/autonomous_agent_api/backend/dependency.py @@ -1,8 +1,16 @@ from backend.app.repositories.agent_repository import AgentRepository +from backend.app.repositories.trigger_repository import TriggerRepository from backend.app.services.agent_service import AgentService +from backend.app.services.trigger_service import TriggerService def get_agent_service() -> AgentService: agent_repository = AgentRepository() agent_service = AgentService(agent_repository) return agent_service + + +def get_trigger_service() -> TriggerService: + trigger_repository = TriggerRepository() + trigger_service = TriggerService(trigger_repository) + return trigger_service diff --git a/autonomous_agent_api/prisma/migrations/20240327040548_0/migration.sql b/autonomous_agent_api/prisma/migrations/20240327040548_0/migration.sql deleted file mode 100644 index 1e9e1055f..000000000 --- a/autonomous_agent_api/prisma/migrations/20240327040548_0/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Drop existing table if it exists -DROP TABLE IF EXISTS "Agent"; - --- CreateTable -CREATE TABLE "Agent" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "action" TEXT[] DEFAULT ARRAY[]::TEXT[], - "triggers" TEXT[] DEFAULT ARRAY[]::TEXT[], - "deleted_at" TIMESTAMP(3), - "created_at" TIMESTAMP(3), - "updated_at" TIMESTAMP(3), - - CONSTRAINT "Agent_pkey" PRIMARY KEY ("id") -); diff --git a/autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql b/autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql new file mode 100644 index 000000000..b76fe985e --- /dev/null +++ b/autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql @@ -0,0 +1,27 @@ +-- CreateEnum +CREATE TYPE "TriggerType" AS ENUM ('CRON', 'TOPIC'); + +-- CreateTable +CREATE TABLE "Agent" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "action" TEXT[] DEFAULT ARRAY[]::TEXT[], + "created_at" TIMESTAMP(3), + "updated_at" TIMESTAMP(3), + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Agent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Trigger" ( + "id" TEXT NOT NULL, + "agent_id" TEXT NOT NULL, + "type" "TriggerType" NOT NULL, + "data" JSONB NOT NULL, + "created_at" TIMESTAMP(3), + "updated_at" TIMESTAMP(3), + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Trigger_pkey" PRIMARY KEY ("id") +); diff --git a/autonomous_agent_api/prisma/schema.prisma b/autonomous_agent_api/prisma/schema.prisma index 9d5d990d2..981bbe02e 100644 --- a/autonomous_agent_api/prisma/schema.prisma +++ b/autonomous_agent_api/prisma/schema.prisma @@ -14,8 +14,22 @@ model Agent { id String @id // UUID name String action String[] @default([]) - triggers String[] @default([]) created_at DateTime? updated_at DateTime? deleted_at DateTime? // Soft deletion field } + +model Trigger { + id String @id + agent_id String + type TriggerType + data Json + created_at DateTime? + updated_at DateTime? + deleted_at DateTime? +} + +enum TriggerType { + CRON + TOPIC +} diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py index 8636d01f2..6f37e83d6 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py @@ -1,10 +1,8 @@ -from typing import List from unittest.mock import MagicMock, AsyncMock from backend.app.controllers.agent_router import AgentRouter -from backend.app.models.response_dto import AgentResponse +from backend.app.models.AgentDto.response_dto import AgentResponse from backend.app.services.agent_service import AgentService -from fastapi import HTTPException import pytest @@ -28,13 +26,11 @@ async def test_list_agents_with_two_agents(agent_service, agent_router): AgentResponse( id="018e8908-5dc9-78c9-b71a-37ebd2149394", name="Agent 1", - triggers=["Description 1"], action=["Description 1"], ), AgentResponse( id="018e8908-7563-7e8a-b87c-b33ac6e6c872", name="Agent 2", - triggers=["Description 2"], action=["Description 2"], ), ] diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py index fd56eb7b3..18842ad68 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py @@ -1,7 +1,7 @@ from pydantic import ValidationError from backend.app.controllers.agent_router import AgentRouter -from backend.app.models.agent_dto import AgentCreateDTO -from backend.app.models.response_dto import AgentResponse +from backend.app.models.AgentDto.agent_dto import AgentCreateDTO +from backend.app.models.AgentDto.response_dto import AgentResponse from backend.app.services.agent_service import AgentService from unittest.mock import MagicMock, AsyncMock import pytest @@ -19,10 +19,8 @@ def agent_router(self, agent_service): async def test_update_agent_with_valid_details(self, agent_service, agent_router): agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" - agent_data = AgentCreateDTO(name="Updated Agent", triggers=["Test Description"], action=["Test Description"]) - updated_agent = AgentResponse( - id=agent_id, name="Updated Agent", triggers=["Test Description"], action=["Test Description"] - ) + agent_data = AgentCreateDTO(name="Updated Agent", action=["Test Description"]) + updated_agent = AgentResponse(id=agent_id, name="Updated Agent", action=["Test Description"]) agent_service.update_agent = AsyncMock(return_value=updated_agent) @@ -36,10 +34,8 @@ async def test_update_agent_should_fail_with_invalid_details(self, agent_service with pytest.raises(ValidationError): # Mock data agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" - agent_data = AgentCreateDTO(name="", triggers=["Test Description"], action=["Test Description"]) - updated_agent = AgentResponse( - id=agent_id, name="Agent", triggers=["Test Description"], action=["Test Description"] - ) + agent_data = AgentCreateDTO(name="", action=["Test Description"]) + updated_agent = AgentResponse(id=agent_id, name="Agent", action=["Test Description"]) agent_service.update_agent = AsyncMock(return_value=updated_agent) diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py index 9904030a3..0b5d15bcc 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py @@ -1,8 +1,8 @@ from pydantic import ValidationError from backend.app.controllers.agent_router import AgentRouter -from backend.app.models.agent_dto import AgentCreateDTO -from backend.app.models.response_dto import AgentResponse +from backend.app.models.AgentDto.agent_dto import AgentCreateDTO +from backend.app.models.AgentDto.response_dto import AgentResponse from backend.app.services.agent_service import AgentService from unittest.mock import MagicMock, AsyncMock import pytest @@ -21,10 +21,8 @@ def agent_router(self, agent_service): @pytest.mark.asyncio async def test_update_agent_with_valid_details(self, agent_service, agent_router): agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" - agent_data = AgentCreateDTO(name="Updated Agent", triggers=["Test Description"], action=["Test Description"]) - updated_agent = AgentResponse( - id=agent_id, name="Updated Agent", triggers=["Test Description"], action=["Test Description"] - ) + agent_data = AgentCreateDTO(name="Updated Agent", action=["Test Description"]) + updated_agent = AgentResponse(id=agent_id, name="Updated Agent", action=["Test Description"]) agent_service.update_agent = AsyncMock(return_value=updated_agent) @@ -39,10 +37,8 @@ async def test_update_agent_should_fail_with_invalid_details(self, agent_service with pytest.raises(ValidationError): # Mock data agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" - agent_data = AgentCreateDTO(name="", triggers=["Test Description"], action=["Test Description"]) - updated_agent = AgentResponse( - id=agent_id, name="Agent", triggers=["Test Description"], action=["Test Description"] - ) + agent_data = AgentCreateDTO(name="", action=["Test Description"]) + updated_agent = AgentResponse(id=agent_id, name="Agent", action=["Test Description"]) agent_service.update_agent = AsyncMock(return_value=updated_agent) From b5c159a7ce75d23e657b139419ebf9125e8d8b43 Mon Sep 17 00:00:00 2001 From: roshan Date: Tue, 2 Apr 2024 14:08:37 +0545 Subject: [PATCH 15/20] created validation for cron expression and topic when creating triggers --- .../backend/app/controllers/agent_router.py | 9 ++-- .../backend/app/controllers/trigger_router.py | 37 +++++--------- .../app/models/TriggerDto/trigger_dto.py | 24 +++++++-- .../app/repositories/trigger_repository.py | 51 ++++++++++--------- .../backend/app/services/trigger_service.py | 22 ++++---- autonomous_agent_api/poetry.lock | 42 ++++++++++++++- autonomous_agent_api/pyproject.toml | 1 + 7 files changed, 113 insertions(+), 73 deletions(-) diff --git a/autonomous_agent_api/backend/app/controllers/agent_router.py b/autonomous_agent_api/backend/app/controllers/agent_router.py index e9efff44d..24c60e4b5 100644 --- a/autonomous_agent_api/backend/app/controllers/agent_router.py +++ b/autonomous_agent_api/backend/app/controllers/agent_router.py @@ -15,13 +15,10 @@ def __init__(self, agent_service: AgentService = get_agent_service(), *args, **k super().__init__(*args, **kwargs) self.agent_service = agent_service - @post("/create_agents/", status_code=HTTPStatus.CREATED) + @post("/agents/", status_code=HTTPStatus.CREATED) async def create_agent(self, agent_data: AgentCreateDTO): - try: - agent = await self.agent_service.create_agent(agent_data) - return agent - except Exception as e: - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + agent = await self.agent_service.create_agent(agent_data) + return agent @get("/agents/", response_model=List[AgentResponse]) async def list_agents(self): diff --git a/autonomous_agent_api/backend/app/controllers/trigger_router.py b/autonomous_agent_api/backend/app/controllers/trigger_router.py index 3956e08d0..164d5413e 100644 --- a/autonomous_agent_api/backend/app/controllers/trigger_router.py +++ b/autonomous_agent_api/backend/app/controllers/trigger_router.py @@ -5,7 +5,7 @@ from fastapi import HTTPException from backend.app.models.TriggerDto.resposne_dto import TriggerResponse -from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerEditDTO +from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerCreateDTO from backend.app.services.trigger_service import TriggerService from backend.dependency import get_trigger_service @@ -15,37 +15,28 @@ def __init__(self, trigger_service: TriggerService = get_trigger_service(), *arg super().__init__(*args, **kwargs) self.trigger_service = trigger_service - @post("/create_trigger/", status_code=HTTPStatus.CREATED) - async def create_trigger(self, trigger_data: TriggerCreateDTO): - trigger = await self.trigger_service.create_trigger(trigger_data) + @post("/agents/{agent_id}/triggers", status_code=HTTPStatus.CREATED) + async def create_trigger(self, agent_id: str, trigger_data: TriggerCreateDTO): + trigger = await self.trigger_service.create_trigger(agent_id, trigger_data) return trigger - @get("/triggers", response_model=List[TriggerResponse]) - async def list_triggers(self): - triggers = await self.trigger_service.list_triggers() - return triggers - - @get("/listTriggersByAgentId/{agent_id}", response_model=List[TriggerResponse]) + @get("/agents/{agent_id}/triggers", response_model=List[TriggerResponse]) async def list_triggers_by_agent_id(self, agent_id: str): triggers = await self.trigger_service.list_triggers_by_agent_id(agent_id) return triggers - @get("/listTriggerByAgentIdAndTriggerId/{agent_id}/{trigger_id}", response_model=TriggerResponse) - async def list_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str): - trigger = await self.trigger_service.list_trigger_by_agent_id_and_trigger_id(agent_id, trigger_id) + @get("/triggers/{trigger_id}", response_model=TriggerResponse) + async def list_trigger_by_trigger_id(self, trigger_id: str): + trigger = await self.trigger_service.list_trigger_by_trigger_id(trigger_id) return trigger - @put("/updateTriggerByAgentIdAndTriggerId/{agent_id}/{trigger_id}", response_model=TriggerResponse) - async def update_trigger_by_agent_id_and_trigger_id( - self, agent_id: str, trigger_id: str, trigger_data: TriggerEditDTO - ): - trigger = await self.trigger_service.update_trigger_by_agent_id_and_trigger_id( - agent_id, trigger_id, trigger_data - ) + @put("/triggers/{trigger_id}", response_model=TriggerResponse) + async def update_trigger_by_agent_id_and_trigger_id(self, trigger_id: str, trigger_data: TriggerCreateDTO): + trigger = await self.trigger_service.update_trigger_by_trigger_id(trigger_id, trigger_data) return trigger - @delete("/DeleteTriggerByAgentIdAndTriggerId/{agent_id}/{trigger_id}", status_code=HTTPStatus.NO_CONTENT) - async def delete_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str): - success = await self.trigger_service.delete_trigger_by_agent_id_and_trigger_id(agent_id, trigger_id) + @delete("/triggers/{trigger_id}", status_code=HTTPStatus.NO_CONTENT) + async def delete_trigger_by_trigger_id(self, trigger_id: str): + success = await self.trigger_service.delete_trigger_by_trigger_id(trigger_id) if not success: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Trigger not found") diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py b/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py index a1cc0942e..ec18cc321 100644 --- a/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py +++ b/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py @@ -1,5 +1,7 @@ +from fastapi import HTTPException from typing import Union -from pydantic import BaseModel, Json +from pydantic import BaseModel +from croniter import croniter class CronTriggerDTO(BaseModel): @@ -14,9 +16,21 @@ class TopicTriggerDTO(BaseModel): class TriggerCreateDTO(BaseModel): type: str data: Union[CronTriggerDTO, TopicTriggerDTO] - agent_id: str -class TriggerEditDTO(BaseModel): - type: str - data: Union[CronTriggerDTO, TopicTriggerDTO] +# validation for cron expression +async def validate_type_CRON(cron_expression: str): + try: + croniter(cron_expression) + except ValueError as e: + raise HTTPException(400, f"Invalid CRON expression: {str(e)}") + + +# validation for Topic +async def validate_type_TOPIC(value: str): + try: + if value.isnumeric(): + raise HTTPException(400, f"Invalid topic :") + + except ValueError as e: + return f"Validation error: {e}" diff --git a/autonomous_agent_api/backend/app/repositories/trigger_repository.py b/autonomous_agent_api/backend/app/repositories/trigger_repository.py index ea6dd38ef..6e4213f7c 100644 --- a/autonomous_agent_api/backend/app/repositories/trigger_repository.py +++ b/autonomous_agent_api/backend/app/repositories/trigger_repository.py @@ -5,7 +5,13 @@ from fastapi import HTTPException from backend.app.models.TriggerDto.resposne_dto import TriggerResponse -from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerEditDTO, CronTriggerDTO, TopicTriggerDTO +from backend.app.models.TriggerDto.trigger_dto import ( + TriggerCreateDTO, + CronTriggerDTO, + TopicTriggerDTO, + validate_type_CRON, + validate_type_TOPIC, +) from backend.config.database import prisma_connection @@ -13,16 +19,21 @@ class TriggerRepository: def __init__(self, db_connection=None): self.db = db_connection or prisma_connection - async def save_trigger(self, trigger_data: TriggerCreateDTO): + async def save_trigger(self, agent_id: str, trigger_data: TriggerCreateDTO): trigger_id = str(uuid.uuid4()) trigger_data_dict = trigger_data.dict() - # Extract the data field and convert it to JSON + if trigger_data.type == "CRON": + await validate_type_CRON(trigger_data.data.frequency) + + if trigger_data.type == "TOPIC": + await validate_type_TOPIC(trigger_data.data.topic) + data_dict = trigger_data_dict.pop("data") data_json = json.dumps(data_dict) - # Assign the JSON data back to the dictionary trigger_data_dict["id"] = trigger_id + trigger_data_dict["agent_id"] = agent_id trigger_data_dict["data"] = data_json trigger_data_dict["created_at"] = datetime.now(timezone.utc) @@ -30,13 +41,9 @@ async def save_trigger(self, trigger_data: TriggerCreateDTO): async with self.db: await self.db.prisma.trigger.create(data=trigger_data_dict) - # Convert the JSON data back to the appropriate DTO object data_object = self._convert_data_to_dto(trigger_data.type, data_dict) - # Create a TriggerResponse object with the converted data - trigger_response = TriggerResponse( - id=trigger_id, agent_id=trigger_data.agent_id, type=trigger_data.type, data=data_object - ) + trigger_response = TriggerResponse(id=trigger_id, agent_id=agent_id, type=trigger_data.type, data=data_object) return trigger_response async def retreive_triggers(self) -> List[TriggerResponse]: @@ -49,36 +56,32 @@ async def retreive_triggers_by_agent_id(self, agent_id: str) -> List[TriggerResp triggers = await self.db.prisma.trigger.find_many(where={"agent_id": agent_id, "deleted_at": None}) return triggers - async def remove_trigger_by_agent_id_trigger_id(self, agent_id: str, trigger_id: str) -> bool: + async def remove_trigger_by_trigger_id(self, trigger_id: str) -> bool: async with self.db: - trigger = await self.db.prisma.trigger.find_first(where={"agent_id": agent_id, "id": trigger_id}) + trigger = await self.db.prisma.trigger.find_first(where={"id": trigger_id}) if trigger is None: return False elif trigger.deleted_at is not None: return True await self.db.prisma.trigger.update( - where={"agent_id": agent_id, "id": trigger_id}, data={"deleted_at": datetime.now(timezone.utc)} + where={"agent_id": trigger.agent_id, "id": trigger_id}, data={"deleted_at": datetime.now(timezone.utc)} ) return True - async def retreive_trigger_by_agent_id_and_trigger_id( - self, agent_id: str, trigger_id: str - ) -> Optional[TriggerResponse]: + async def retreive_trigger_by_trigger_id(self, trigger_id: str) -> Optional[TriggerResponse]: async with self.db: - trigger = await self.db.prisma.trigger.find_first( - where={"agent_id": agent_id, "id": trigger_id, "deleted_at": None} - ) + trigger = await self.db.prisma.trigger.find_first(where={"id": trigger_id, "deleted_at": None}) if trigger is None: raise HTTPException(status_code=404, detail="Trigger not found") else: return trigger - async def modify_trigger_by_agent_id_and_trigger_id( - self, agent_id: str, trigger_id: str, trigger_data: TriggerEditDTO + async def modify_trigger_by_trigger_id( + self, trigger_id: str, trigger_data: TriggerCreateDTO ) -> Optional[TriggerResponse]: async with self.db: - trigger = await self.db.prisma.trigger.find_first(where={"agent_id": agent_id, "id": trigger_id}) + trigger = await self.db.prisma.trigger.find_first(where={"id": trigger_id}) if trigger is None or trigger.deleted_at is not None: raise HTTPException(status_code=404, detail="Trigger not found") updated_data_dict = trigger_data.dict() @@ -88,13 +91,11 @@ async def modify_trigger_by_agent_id_and_trigger_id( updated_data_dict["data"] = data_json updated_data_dict["updated_at"] = datetime.now(timezone.utc) - updated_trigger = await self.db.prisma.trigger.update( - where={"agent_id": agent_id, "id": trigger_id}, data=updated_data_dict - ) + await self.db.prisma.trigger.update(where={"id": trigger_id}, data=updated_data_dict) # Create a TriggerResponse object with the converted data trigger_response = TriggerResponse( - id=trigger_id, agent_id=agent_id, type=trigger_data.type, data=trigger_data.data + id=trigger_id, agent_id=trigger.agent_id, type=trigger_data.type, data=trigger_data.data ) return trigger_response diff --git a/autonomous_agent_api/backend/app/services/trigger_service.py b/autonomous_agent_api/backend/app/services/trigger_service.py index c8f49e57e..51b546db7 100644 --- a/autonomous_agent_api/backend/app/services/trigger_service.py +++ b/autonomous_agent_api/backend/app/services/trigger_service.py @@ -1,7 +1,7 @@ from typing import List from backend.app.models.TriggerDto.resposne_dto import TriggerResponse -from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerEditDTO +from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO from backend.app.repositories.trigger_repository import TriggerRepository @@ -9,8 +9,8 @@ class TriggerService: def __init__(self, trigger_repository: TriggerRepository): self.trigger_repository = trigger_repository - async def create_trigger(self, trigger_data: TriggerCreateDTO) -> TriggerResponse: - return await self.trigger_repository.save_trigger(trigger_data) + async def create_trigger(self, agent_id: str, trigger_data: TriggerCreateDTO) -> TriggerResponse: + return await self.trigger_repository.save_trigger(agent_id, trigger_data) async def list_triggers(self) -> List[TriggerResponse]: return await self.trigger_repository.retreive_triggers() @@ -18,15 +18,11 @@ async def list_triggers(self) -> List[TriggerResponse]: async def list_triggers_by_agent_id(self, agent_id: str) -> List[TriggerResponse]: return await self.trigger_repository.retreive_triggers_by_agent_id(agent_id) - async def delete_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str) -> bool: - return await self.trigger_repository.remove_trigger_by_agent_id_trigger_id(agent_id, trigger_id) + async def delete_trigger_by_trigger_id(self, trigger_id: str) -> bool: + return await self.trigger_repository.remove_trigger_by_trigger_id(trigger_id) - async def list_trigger_by_agent_id_and_trigger_id(self, agent_id: str, trigger_id: str) -> TriggerResponse: - return await self.trigger_repository.retreive_trigger_by_agent_id_and_trigger_id(agent_id, trigger_id) + async def list_trigger_by_trigger_id(self, trigger_id: str) -> TriggerResponse: + return await self.trigger_repository.retreive_trigger_by_trigger_id(trigger_id) - async def update_trigger_by_agent_id_and_trigger_id( - self, agent_id: str, trigger_id: str, trigger_data: TriggerEditDTO - ) -> TriggerResponse: - return await self.trigger_repository.modify_trigger_by_agent_id_and_trigger_id( - agent_id, trigger_id, trigger_data - ) + async def update_trigger_by_trigger_id(self, trigger_id: str, trigger_data: TriggerCreateDTO) -> TriggerResponse: + return await self.trigger_repository.modify_trigger_by_trigger_id(trigger_id, trigger_data) diff --git a/autonomous_agent_api/poetry.lock b/autonomous_agent_api/poetry.lock index d9b0aac64..2a9da6488 100644 --- a/autonomous_agent_api/poetry.lock +++ b/autonomous_agent_api/poetry.lock @@ -499,6 +499,21 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "croniter" +version = "2.0.3" +description = "croniter provides iteration for datetime object with cron like format" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "croniter-2.0.3-py2.py3-none-any.whl", hash = "sha256:84dc95b2eb6760144cc01eca65a6b9cc1619c93b2dc37d8a27f4319b3eb740de"}, + {file = "croniter-2.0.3.tar.gz", hash = "sha256:28763ad39c404e159140874f08010cfd8a18f4c2a7cea1ce73e9506a4380cfc1"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = ">2021.1" + [[package]] name = "docutils" version = "0.19" @@ -1638,6 +1653,20 @@ files = [ pytest = ">=5.0.0" python-dotenv = ">=0.9.1" +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1849,6 +1878,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2447,4 +2487,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "5fc1a77c6954037702b82110f3c48616bbe266ccc674a36e7df6a1232c671c45" +content-hash = "6c8af4e830b2c8833d151b4c965a9a1f049dea057373eee373e3da745faac73f" diff --git a/autonomous_agent_api/pyproject.toml b/autonomous_agent_api/pyproject.toml index ca46b28d4..2a455644a 100644 --- a/autonomous_agent_api/pyproject.toml +++ b/autonomous_agent_api/pyproject.toml @@ -30,6 +30,7 @@ aiohttp = "~3.9.3" prisma = "^0.13.0" pytest-dotenv = "^0.5.2" classy-fastapi = "^0.6.1" +croniter = "^2.0.3" [tool.poetry.group.dev.dependencies] pytest = "~7.4.0" From 42411207b89e75660709a99878d0afc96ea56d0f Mon Sep 17 00:00:00 2001 From: roshan Date: Tue, 2 Apr 2024 14:56:55 +0545 Subject: [PATCH 16/20] updated changelog md --- autonomous_agent_api/CHANGELOG.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/autonomous_agent_api/CHANGELOG.md b/autonomous_agent_api/CHANGELOG.md index 0c9589c1d..15d3310cd 100644 --- a/autonomous_agent_api/CHANGELOG.md +++ b/autonomous_agent_api/CHANGELOG.md @@ -12,10 +12,18 @@ - Created Agent prisma model - Added agent_router - created crud Api endpoints - POST api for agent creation - GET api for listing agents - GET api for getting agent properties. - PUT api to update agent properties. - DELETE api for soft deletion of agent. - -created dtos model for request and response inside model directory + - POST api for agent creation + - GET api for listing agents + - GET api for getting agent properties. + - PUT api to update agent properties. + - DELETE api for soft deletion of agent. + - created dtos model for request and response inside model directory + +# TITLE - TEST Setup and Test cases for Agent Api , Date - 2024-03-28/29 - 04-01 + - Made setup to run the test cases + - Wrote Unit test for API methods of Agent +# TITLE - Trigger/API crud operation , Date - 2024-04-01/02 + - Made table for Triggers + - Created router,service,repository for trigger CRUD Action + - Created the validation function for cron expression and kafka topic while creating the trigger by agent From cc99f5646c74a4e7919de15330c772c006b6221d Mon Sep 17 00:00:00 2001 From: roshan Date: Wed, 3 Apr 2024 06:32:00 +0545 Subject: [PATCH 17/20] removed blank file --- .../backend/app/controllers/cron_trigger_config_router.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 autonomous_agent_api/backend/app/controllers/cron_trigger_config_router.py diff --git a/autonomous_agent_api/backend/app/controllers/cron_trigger_config_router.py b/autonomous_agent_api/backend/app/controllers/cron_trigger_config_router.py deleted file mode 100644 index e69de29bb..000000000 From f7eda5a23b45d832783990442ddfca3b97d91120 Mon Sep 17 00:00:00 2001 From: roshan Date: Wed, 3 Apr 2024 06:43:16 +0545 Subject: [PATCH 18/20] updated poetry lock for croniter package --- autonomous_agent_api/poetry.lock | 92 ++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/autonomous_agent_api/poetry.lock b/autonomous_agent_api/poetry.lock index 214a83e6e..3432aedd0 100644 --- a/autonomous_agent_api/poetry.lock +++ b/autonomous_agent_api/poetry.lock @@ -499,6 +499,21 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "croniter" +version = "2.0.3" +description = "croniter provides iteration for datetime object with cron like format" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "croniter-2.0.3-py2.py3-none-any.whl", hash = "sha256:84dc95b2eb6760144cc01eca65a6b9cc1619c93b2dc37d8a27f4319b3eb740de"}, + {file = "croniter-2.0.3.tar.gz", hash = "sha256:28763ad39c404e159140874f08010cfd8a18f4c2a7cea1ce73e9506a4380cfc1"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = ">2021.1" + [[package]] name = "docutils" version = "0.19" @@ -716,6 +731,41 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "fsspec" +version = "2024.3.1" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, + {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + [[package]] name = "gunicorn" version = "20.1.0" @@ -1638,6 +1688,20 @@ files = [ pytest = ">=5.0.0" python-dotenv = ">=0.9.1" +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1748,20 +1812,19 @@ files = [ [[package]] name = "pyyaml-include" -version = "1.4.1" -description = "Extending PyYAML with a custom constructor for including YAML files within YAML files" +version = "2.0" +description = "An extending constructor of PyYAML: include other YAML files into current YAML document" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pyyaml-include-1.4.1.tar.gz", hash = "sha256:1a96e33a99a3e56235f5221273832464025f02ff3d8539309a3bf00dec624471"}, - {file = "pyyaml_include-1.4.1-py3-none-any.whl", hash = "sha256:323c7f3a19c82fbc4d73abbaab7ef4f793e146a13383866831631b26ccc7fb00"}, + {file = "pyyaml-include-2.0.tar.gz", hash = "sha256:e826f0b3121a3818e04a0f45169a8af59b90961a91c7f4984086226c6c72e600"}, + {file = "pyyaml_include-2.0-py3-none-any.whl", hash = "sha256:b053fb227754af99d3d3326745e1289f91e839b430888eca0b195ebb925141bd"}, ] [package.dependencies] +fsspec = ">=2021.04.0" PyYAML = ">=6.0,<7.0" - -[package.extras] -toml = ["toml"] +typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "questionary" @@ -1849,6 +1912,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2447,4 +2521,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ee6533f48aa232f19dc21dab76dce4b59f0b52c4097884fd647b18a0ae502807" +content-hash = "6c8af4e830b2c8833d151b4c965a9a1f049dea057373eee373e3da745faac73f" From 297e0214d9d901cd447e1bb6856ccf86045331ca Mon Sep 17 00:00:00 2001 From: roshan Date: Thu, 4 Apr 2024 13:05:39 +0545 Subject: [PATCH 19/20] black lint formatted four files --- .../backend/app/repositories/agent_repository.py | 2 +- .../unit/app/controllers/agent_router_test/test_agent_list.py | 2 +- .../unit/app/controllers/agent_router_test/test_create_agent.py | 2 +- .../app/controllers/agent_router_test/test_editing_agent.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autonomous_agent_api/backend/app/repositories/agent_repository.py b/autonomous_agent_api/backend/app/repositories/agent_repository.py index ea9cff560..e37328bf3 100644 --- a/autonomous_agent_api/backend/app/repositories/agent_repository.py +++ b/autonomous_agent_api/backend/app/repositories/agent_repository.py @@ -60,4 +60,4 @@ async def remove_agent(self, agent_id: str) -> bool: await self.db.prisma.agstringent.update( where={"id": agent_id}, data={"deleted_at": datetime.now(timezone.utc)} ) - return True \ No newline at end of file + return True diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py index b99816a41..6f37e83d6 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py @@ -43,4 +43,4 @@ async def test_list_agents_with_two_agents(agent_service, agent_router): agent_service.list_agents.assert_called_once() # Assert that the returned agents match the mock_agents - assert agents == mock_agents \ No newline at end of file + assert agents == mock_agents diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py index b99816a41..6f37e83d6 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py @@ -43,4 +43,4 @@ async def test_list_agents_with_two_agents(agent_service, agent_router): agent_service.list_agents.assert_called_once() # Assert that the returned agents match the mock_agents - assert agents == mock_agents \ No newline at end of file + assert agents == mock_agents diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py index f8c152d29..0b5d15bcc 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py @@ -46,4 +46,4 @@ async def test_update_agent_should_fail_with_invalid_details(self, agent_service agent_service.update_agent.assert_called_once_with(agent_id, agent_data) - assert result == updated_agent \ No newline at end of file + assert result == updated_agent From 76ed0183d2ead382e5625660e801ce0630b72b67 Mon Sep 17 00:00:00 2001 From: roshan Date: Thu, 4 Apr 2024 14:20:50 +0545 Subject: [PATCH 20/20] made changes according to feedback --- .../backend/app/controllers/agent_router.py | 4 +- .../backend/app/controllers/trigger_router.py | 12 +-- .../backend/app/models/TriggerDto/__init__.py | 0 .../backend/app/models/__init__.py | 5 +- .../models/{AgentDto => agent}/__init__.py | 0 .../models/{AgentDto => agent}/agent_dto.py | 0 .../{AgentDto => agent}/response_dto.py | 0 .../backend/app/models/trigger/__init__.py | 1 + .../{TriggerDto => trigger}/resposne_dto.py | 2 +- .../{TriggerDto => trigger}/trigger_dto.py | 0 .../app/repositories/agent_repository.py | 4 +- .../app/repositories/trigger_repository.py | 10 +-- .../backend/app/services/agent_service.py | 4 +- .../backend/app/services/trigger_service.py | 14 ++-- .../migration.sql | 8 +- autonomous_agent_api/prisma/schema.prisma | 8 +- .../agent_router_test/test_agent_list.py | 2 +- .../agent_router_test/test_create_agent.py | 75 ++++++++++--------- .../agent_router_test/test_editing_agent.py | 4 +- 19 files changed, 79 insertions(+), 74 deletions(-) delete mode 100644 autonomous_agent_api/backend/app/models/TriggerDto/__init__.py rename autonomous_agent_api/backend/app/models/{AgentDto => agent}/__init__.py (100%) rename autonomous_agent_api/backend/app/models/{AgentDto => agent}/agent_dto.py (100%) rename autonomous_agent_api/backend/app/models/{AgentDto => agent}/response_dto.py (100%) create mode 100644 autonomous_agent_api/backend/app/models/trigger/__init__.py rename autonomous_agent_api/backend/app/models/{TriggerDto => trigger}/resposne_dto.py (78%) rename autonomous_agent_api/backend/app/models/{TriggerDto => trigger}/trigger_dto.py (100%) rename autonomous_agent_api/prisma/migrations/{20240402034446_initial_schema => 20240404083450_initial_schema}/migration.sql (76%) diff --git a/autonomous_agent_api/backend/app/controllers/agent_router.py b/autonomous_agent_api/backend/app/controllers/agent_router.py index 24c60e4b5..29fc6d145 100644 --- a/autonomous_agent_api/backend/app/controllers/agent_router.py +++ b/autonomous_agent_api/backend/app/controllers/agent_router.py @@ -4,9 +4,9 @@ from classy_fastapi import Routable, get, post, put, delete from fastapi import HTTPException -from backend.app.models.AgentDto.agent_dto import AgentCreateDTO +from backend.app.models.agent.agent_dto import AgentCreateDTO from backend.app.services.agent_service import AgentService -from backend.app.models.AgentDto.response_dto import AgentResponse +from backend.app.models.agent.response_dto import AgentResponse from backend.dependency import get_agent_service diff --git a/autonomous_agent_api/backend/app/controllers/trigger_router.py b/autonomous_agent_api/backend/app/controllers/trigger_router.py index 164d5413e..bbbe13fe2 100644 --- a/autonomous_agent_api/backend/app/controllers/trigger_router.py +++ b/autonomous_agent_api/backend/app/controllers/trigger_router.py @@ -4,8 +4,8 @@ from classy_fastapi import Routable, get, post, put, delete from fastapi import HTTPException -from backend.app.models.TriggerDto.resposne_dto import TriggerResponse -from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO, TriggerCreateDTO +from backend.app.models import TriggerResponse +from backend.app.models import TriggerCreateDTO from backend.app.services.trigger_service import TriggerService from backend.dependency import get_trigger_service @@ -27,16 +27,16 @@ async def list_triggers_by_agent_id(self, agent_id: str): @get("/triggers/{trigger_id}", response_model=TriggerResponse) async def list_trigger_by_trigger_id(self, trigger_id: str): - trigger = await self.trigger_service.list_trigger_by_trigger_id(trigger_id) + trigger = await self.trigger_service.list_trigger_by_id(trigger_id) return trigger @put("/triggers/{trigger_id}", response_model=TriggerResponse) - async def update_trigger_by_agent_id_and_trigger_id(self, trigger_id: str, trigger_data: TriggerCreateDTO): - trigger = await self.trigger_service.update_trigger_by_trigger_id(trigger_id, trigger_data) + async def update_trigger_by_trigger_id(self, trigger_id: str, trigger_data: TriggerCreateDTO): + trigger = await self.trigger_service.update_trigger_by_id(trigger_id, trigger_data) return trigger @delete("/triggers/{trigger_id}", status_code=HTTPStatus.NO_CONTENT) async def delete_trigger_by_trigger_id(self, trigger_id: str): - success = await self.trigger_service.delete_trigger_by_trigger_id(trigger_id) + success = await self.trigger_service.delete_by_id(trigger_id) if not success: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Trigger not found") diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/__init__.py b/autonomous_agent_api/backend/app/models/TriggerDto/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/autonomous_agent_api/backend/app/models/__init__.py b/autonomous_agent_api/backend/app/models/__init__.py index 468f57eaa..2c19f0a78 100644 --- a/autonomous_agent_api/backend/app/models/__init__.py +++ b/autonomous_agent_api/backend/app/models/__init__.py @@ -1 +1,4 @@ -"""Application implementation - models.""" +# backend/app/models/__init__.py + +from .trigger.resposne_dto import TriggerResponse +from .trigger.trigger_dto import TriggerCreateDTO, TopicTriggerDTO, CronTriggerDTO diff --git a/autonomous_agent_api/backend/app/models/AgentDto/__init__.py b/autonomous_agent_api/backend/app/models/agent/__init__.py similarity index 100% rename from autonomous_agent_api/backend/app/models/AgentDto/__init__.py rename to autonomous_agent_api/backend/app/models/agent/__init__.py diff --git a/autonomous_agent_api/backend/app/models/AgentDto/agent_dto.py b/autonomous_agent_api/backend/app/models/agent/agent_dto.py similarity index 100% rename from autonomous_agent_api/backend/app/models/AgentDto/agent_dto.py rename to autonomous_agent_api/backend/app/models/agent/agent_dto.py diff --git a/autonomous_agent_api/backend/app/models/AgentDto/response_dto.py b/autonomous_agent_api/backend/app/models/agent/response_dto.py similarity index 100% rename from autonomous_agent_api/backend/app/models/AgentDto/response_dto.py rename to autonomous_agent_api/backend/app/models/agent/response_dto.py diff --git a/autonomous_agent_api/backend/app/models/trigger/__init__.py b/autonomous_agent_api/backend/app/models/trigger/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/autonomous_agent_api/backend/app/models/trigger/__init__.py @@ -0,0 +1 @@ + diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py b/autonomous_agent_api/backend/app/models/trigger/resposne_dto.py similarity index 78% rename from autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py rename to autonomous_agent_api/backend/app/models/trigger/resposne_dto.py index bdaceffbf..61ca76c8a 100644 --- a/autonomous_agent_api/backend/app/models/TriggerDto/resposne_dto.py +++ b/autonomous_agent_api/backend/app/models/trigger/resposne_dto.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from typing import Union -from backend.app.models.TriggerDto.trigger_dto import CronTriggerDTO, TopicTriggerDTO +from backend.app.models.trigger.trigger_dto import CronTriggerDTO, TopicTriggerDTO class TriggerResponse(BaseModel): diff --git a/autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py b/autonomous_agent_api/backend/app/models/trigger/trigger_dto.py similarity index 100% rename from autonomous_agent_api/backend/app/models/TriggerDto/trigger_dto.py rename to autonomous_agent_api/backend/app/models/trigger/trigger_dto.py diff --git a/autonomous_agent_api/backend/app/repositories/agent_repository.py b/autonomous_agent_api/backend/app/repositories/agent_repository.py index e37328bf3..67726dae2 100644 --- a/autonomous_agent_api/backend/app/repositories/agent_repository.py +++ b/autonomous_agent_api/backend/app/repositories/agent_repository.py @@ -2,8 +2,8 @@ from datetime import datetime, timezone from typing import List, Optional from fastapi import HTTPException -from backend.app.models.AgentDto.agent_dto import AgentCreateDTO -from backend.app.models.AgentDto.response_dto import AgentResponse +from backend.app.models.agent.agent_dto import AgentCreateDTO +from backend.app.models.agent.response_dto import AgentResponse from backend.config.database import prisma_connection diff --git a/autonomous_agent_api/backend/app/repositories/trigger_repository.py b/autonomous_agent_api/backend/app/repositories/trigger_repository.py index 6e4213f7c..2d3387531 100644 --- a/autonomous_agent_api/backend/app/repositories/trigger_repository.py +++ b/autonomous_agent_api/backend/app/repositories/trigger_repository.py @@ -4,8 +4,8 @@ from typing import List, Optional, Union from fastapi import HTTPException -from backend.app.models.TriggerDto.resposne_dto import TriggerResponse -from backend.app.models.TriggerDto.trigger_dto import ( +from backend.app.models.trigger.resposne_dto import TriggerResponse +from backend.app.models.trigger.trigger_dto import ( TriggerCreateDTO, CronTriggerDTO, TopicTriggerDTO, @@ -69,7 +69,7 @@ async def remove_trigger_by_trigger_id(self, trigger_id: str) -> bool: ) return True - async def retreive_trigger_by_trigger_id(self, trigger_id: str) -> Optional[TriggerResponse]: + async def retreive_trigger_by_id(self, trigger_id: str) -> Optional[TriggerResponse]: async with self.db: trigger = await self.db.prisma.trigger.find_first(where={"id": trigger_id, "deleted_at": None}) if trigger is None: @@ -77,9 +77,7 @@ async def retreive_trigger_by_trigger_id(self, trigger_id: str) -> Optional[Trig else: return trigger - async def modify_trigger_by_trigger_id( - self, trigger_id: str, trigger_data: TriggerCreateDTO - ) -> Optional[TriggerResponse]: + async def modify_trigger_by_id(self, trigger_id: str, trigger_data: TriggerCreateDTO) -> Optional[TriggerResponse]: async with self.db: trigger = await self.db.prisma.trigger.find_first(where={"id": trigger_id}) if trigger is None or trigger.deleted_at is not None: diff --git a/autonomous_agent_api/backend/app/services/agent_service.py b/autonomous_agent_api/backend/app/services/agent_service.py index 1462db1df..8f015f7f8 100644 --- a/autonomous_agent_api/backend/app/services/agent_service.py +++ b/autonomous_agent_api/backend/app/services/agent_service.py @@ -1,8 +1,8 @@ # agent_service.py from typing import List -from backend.app.models.AgentDto.agent_dto import AgentCreateDTO -from backend.app.models.AgentDto.response_dto import AgentResponse +from backend.app.models.agent.agent_dto import AgentCreateDTO +from backend.app.models.agent.response_dto import AgentResponse from backend.app.repositories.agent_repository import AgentRepository diff --git a/autonomous_agent_api/backend/app/services/trigger_service.py b/autonomous_agent_api/backend/app/services/trigger_service.py index 51b546db7..7ac116d6d 100644 --- a/autonomous_agent_api/backend/app/services/trigger_service.py +++ b/autonomous_agent_api/backend/app/services/trigger_service.py @@ -1,7 +1,7 @@ from typing import List -from backend.app.models.TriggerDto.resposne_dto import TriggerResponse -from backend.app.models.TriggerDto.trigger_dto import TriggerCreateDTO +from backend.app.models.trigger.resposne_dto import TriggerResponse +from backend.app.models.trigger.trigger_dto import TriggerCreateDTO from backend.app.repositories.trigger_repository import TriggerRepository @@ -18,11 +18,11 @@ async def list_triggers(self) -> List[TriggerResponse]: async def list_triggers_by_agent_id(self, agent_id: str) -> List[TriggerResponse]: return await self.trigger_repository.retreive_triggers_by_agent_id(agent_id) - async def delete_trigger_by_trigger_id(self, trigger_id: str) -> bool: + async def delete_by_id(self, trigger_id: str) -> bool: return await self.trigger_repository.remove_trigger_by_trigger_id(trigger_id) - async def list_trigger_by_trigger_id(self, trigger_id: str) -> TriggerResponse: - return await self.trigger_repository.retreive_trigger_by_trigger_id(trigger_id) + async def list_trigger_by_id(self, trigger_id: str) -> TriggerResponse: + return await self.trigger_repository.retreive_trigger_by_id(trigger_id) - async def update_trigger_by_trigger_id(self, trigger_id: str, trigger_data: TriggerCreateDTO) -> TriggerResponse: - return await self.trigger_repository.modify_trigger_by_trigger_id(trigger_id, trigger_data) + async def update_trigger_by_id(self, trigger_id: str, trigger_data: TriggerCreateDTO) -> TriggerResponse: + return await self.trigger_repository.modify_trigger_by_id(trigger_id, trigger_data) diff --git a/autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql b/autonomous_agent_api/prisma/migrations/20240404083450_initial_schema/migration.sql similarity index 76% rename from autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql rename to autonomous_agent_api/prisma/migrations/20240404083450_initial_schema/migration.sql index b76fe985e..196aae208 100644 --- a/autonomous_agent_api/prisma/migrations/20240402034446_initial_schema/migration.sql +++ b/autonomous_agent_api/prisma/migrations/20240404083450_initial_schema/migration.sql @@ -6,8 +6,8 @@ CREATE TABLE "Agent" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "action" TEXT[] DEFAULT ARRAY[]::TEXT[], - "created_at" TIMESTAMP(3), - "updated_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL, "deleted_at" TIMESTAMP(3), CONSTRAINT "Agent_pkey" PRIMARY KEY ("id") @@ -19,8 +19,8 @@ CREATE TABLE "Trigger" ( "agent_id" TEXT NOT NULL, "type" "TriggerType" NOT NULL, "data" JSONB NOT NULL, - "created_at" TIMESTAMP(3), - "updated_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL, "deleted_at" TIMESTAMP(3), CONSTRAINT "Trigger_pkey" PRIMARY KEY ("id") diff --git a/autonomous_agent_api/prisma/schema.prisma b/autonomous_agent_api/prisma/schema.prisma index 981bbe02e..63ee09013 100644 --- a/autonomous_agent_api/prisma/schema.prisma +++ b/autonomous_agent_api/prisma/schema.prisma @@ -14,8 +14,8 @@ model Agent { id String @id // UUID name String action String[] @default([]) - created_at DateTime? - updated_at DateTime? + created_at DateTime + updated_at DateTime deleted_at DateTime? // Soft deletion field } @@ -24,8 +24,8 @@ model Trigger { agent_id String type TriggerType data Json - created_at DateTime? - updated_at DateTime? + created_at DateTime + updated_at DateTime deleted_at DateTime? } diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py index 6f37e83d6..53533b37d 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_agent_list.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, AsyncMock from backend.app.controllers.agent_router import AgentRouter -from backend.app.models.AgentDto.response_dto import AgentResponse +from backend.app.models.agent.response_dto import AgentResponse from backend.app.services.agent_service import AgentService import pytest diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py index 6f37e83d6..5d4dfb09d 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_create_agent.py @@ -1,46 +1,49 @@ -from unittest.mock import MagicMock, AsyncMock +from pydantic import ValidationError from backend.app.controllers.agent_router import AgentRouter -from backend.app.models.AgentDto.response_dto import AgentResponse +from backend.app.models.agent.agent_dto import AgentCreateDTO +from backend.app.models.agent.response_dto import AgentResponse from backend.app.services.agent_service import AgentService - +from unittest.mock import MagicMock, AsyncMock import pytest -@pytest.fixture -def agent_service(): - mock_service = MagicMock(spec=AgentService) - return mock_service +@pytest.mark.asyncio +class TestAgentEditRouter: + @pytest.fixture + def agent_service(self): + return MagicMock(spec=AgentService) + @pytest.fixture + def agent_router(self, agent_service): + return AgentRouter(agent_service) -@pytest.fixture -def agent_router(agent_service): - return AgentRouter(agent_service) + @pytest.mark.asyncio + async def test_create_agent_with_valid_details(self, agent_service, agent_router): + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + agent_data = AgentCreateDTO(name="Agent", action=["Test Description"]) + created_agent = AgentResponse(id=agent_id, name="Agent", action=["Test Description"]) + agent_service.update_agent = AsyncMock(return_value=created_agent) -# test for listing the agents -@pytest.mark.asyncio -async def test_list_agents_with_two_agents(agent_service, agent_router): - # Mocking the list_agents method to return a list of mock agents - mock_agents = [ - AgentResponse( - id="018e8908-5dc9-78c9-b71a-37ebd2149394", - name="Agent 1", - action=["Description 1"], - ), - AgentResponse( - id="018e8908-7563-7e8a-b87c-b33ac6e6c872", - name="Agent 2", - action=["Description 2"], - ), - ] - agent_service.list_agents = AsyncMock(return_value=mock_agents) - - # Call the list_agents method - agents = await agent_router.list_agents() - - # calling method - agent_service.list_agents.assert_called_once() - - # Assert that the returned agents match the mock_agents - assert agents == mock_agents + result = await agent_router.create_agent(agent_id, agent_data) + + agent_service.update_agent.assert_called_once_with(agent_id, agent_data) + + assert result == created_agent + + @pytest.mark.asyncio + async def test_create_agent_should_fail_with_invalid_details(self, agent_service, agent_router): + with pytest.raises(ValidationError): + # Mock data + agent_id = "018e8909-549b-7b9f-8fab-5499f53a8244" + agent_data = AgentCreateDTO(name="", action=["Test Description"]) + created_agent = AgentResponse(id=agent_id, name="Agent", action=["Test Description"]) + + agent_service.update_agent = AsyncMock(return_value=created_agent) + + result = await agent_router.update_agent(agent_id, agent_data) + + agent_service.update_agent.assert_called_once_with(agent_id, agent_data) + + assert result == created_agent diff --git a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py index 0b5d15bcc..4bebe90a1 100644 --- a/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py +++ b/autonomous_agent_api/tests/unit/app/controllers/agent_router_test/test_editing_agent.py @@ -1,8 +1,8 @@ from pydantic import ValidationError from backend.app.controllers.agent_router import AgentRouter -from backend.app.models.AgentDto.agent_dto import AgentCreateDTO -from backend.app.models.AgentDto.response_dto import AgentResponse +from backend.app.models.agent.agent_dto import AgentCreateDTO +from backend.app.models.agent.response_dto import AgentResponse from backend.app.services.agent_service import AgentService from unittest.mock import MagicMock, AsyncMock import pytest