diff --git a/.github/workflows/automated_release.yml b/.github/workflows/automated_release.yml new file mode 100644 index 0000000000..e194b0e5f5 --- /dev/null +++ b/.github/workflows/automated_release.yml @@ -0,0 +1,73 @@ +name: Automated Release + +on: + push: + branches: + - main + +jobs: + check-and-release: + runs-on: ubuntu-latest + steps: + # Checkout + - name: Checkout code + uses: actions/checkout@v3 + + # Get the latest release version + - name: Get latest release version + id: get_latest_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + latest_release=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' || echo "v0.0.0") + echo "latest_release=${latest_release}" >> $GITHUB_ENV + + # Extract version from _RELEASE_VERSION constant in main.py + - name: Extract version from main.py + id: get_submitted_version + run: | + submitted_version=$(grep '_RELEASE_VERSION' src/backend/main.py | sed -E 's/.*"([^"]+)".*/\1/') + echo "submitted_version=${submitted_version}" >> $GITHUB_ENV + + # Check if new version is greater + - name: Check if new version is greater + id: check_version + run: | + latest_version_number=$(echo "$latest_release" | sed 's/^v//') + submitted_version_number=$(echo "$submitted_version" | sed 's/^v//') + # Get major, minor, and patch from the versions + IFS='.' read -r l_major l_minor l_patch <<<"$latest_version_number" + IFS='.' read -r f_major f_minor f_patch <<<"$submitted_version_number" + # Compare major, minor, and patch versions + if [ "$f_major" -gt "$l_major" ] || + ([ "$f_major" -eq "$l_major" ] && [ "$f_minor" -gt "$l_minor" ]) || + ([ "$f_major" -eq "$l_major" ] && [ "$f_minor" -eq "$l_minor" ] && [ "$f_patch" -gt "$l_patch" ]); then + echo "new_version_needed=true" >> $GITHUB_ENV + echo "NEED TO CREATE A NEW RELEASE." + else + echo "new_version_needed=false" >> $GITHUB_ENV + echo "NO NEED TO CREATE A NEW RELEASE." + exit 0 + fi + + # Create a new release if the version is greater + - name: Create new release + if: env.new_version_needed == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + release_date=$(date +'%Y-%m-%d') + release_title="${release_date} (${submitted_version})" + description="### Full Changelog: https://github.com/${GITHUB_REPOSITORY}/compare/${latest_release}...${submitted_version}" + latest_release_date=$(gh release view "${latest_release}" --json createdAt --jq '.createdAt') + changelog=$(gh pr list --state merged --base main --search "merged:>$latest_release_date" --json title,number,url,author --jq '.[] | "- PR #" + (.number|tostring) + " by @" + (.author.login) + ": " + .title') + if [[ -z "$changelog" ]]; then + echo "No pull requests found since the last release." + changelog="No pull requests found since the last release." + fi + description="## What's Changed:\n${changelog}\n${description}" + echo "Creating release with title: $release_title" + gh release create "${submitted_version}" \ + --title "$release_title" \ + --notes "$(echo -e "$description")" + echo "Release created successfully." \ No newline at end of file diff --git a/Makefile b/Makefile index 65ba6df261..8c5586c274 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ run-community-tests-debug: .PHONY: run-integration-tests run-integration-tests: - docker compose run --rm --build backend poetry run pytest -c src/backend/pytest_integration.ini src/backend/tests/integration/$(file) + poetry run pytest -c src/backend/pytest_integration.ini src/backend/tests/integration/$(file) .PHONY: test-db test-db: diff --git a/src/backend/alembic/versions/2024_08_01_117f0d9b1d3d_seed_deployments_data.py b/src/backend/alembic/versions/2024_08_01_117f0d9b1d3d_seed_deployments_data.py index 95a99f8b76..8e300def5a 100644 --- a/src/backend/alembic/versions/2024_08_01_117f0d9b1d3d_seed_deployments_data.py +++ b/src/backend/alembic/versions/2024_08_01_117f0d9b1d3d_seed_deployments_data.py @@ -10,9 +10,9 @@ from alembic import op -from backend.database_models.seeders.deployments_models_seed import ( - delete_default_models, - deployments_models_seed, +from backend.database_models.seeders.organization_seed import ( + delete_default_organization, + seed_default_organization, ) # revision identifiers, used by Alembic. @@ -23,8 +23,8 @@ def upgrade() -> None: - deployments_models_seed(op) + seed_default_organization(op) def downgrade() -> None: - delete_default_models(op) + delete_default_organization(op) diff --git a/src/backend/database_models/seeders/deployments_models_seed.py b/src/backend/database_models/seeders/deployments_models_seed.py deleted file mode 100644 index 400735f52a..0000000000 --- a/src/backend/database_models/seeders/deployments_models_seed.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy.orm import Session - -from backend.database_models import Deployment, Model, Organization - - -def deployments_models_seed(op): - """ - Seed default deployments, models, organization, user and agent. - """ - # Previously we would seed the default deployments and models here. We've changed this - # behaviour during a refactor of the deployments module so that deployments and models - # are inserted when they're first used. This solves an issue where seed data would - # sometimes be inserted with invalid config data. - pass - - -def delete_default_models(op): - """ - Delete deployments and models. - """ - session = Session(op.get_bind()) - session.query(Deployment).delete() - session.query(Model).delete() - session.query(Organization).filter_by(id="default").delete() - session.commit() diff --git a/src/backend/database_models/seeders/organization_seed.py b/src/backend/database_models/seeders/organization_seed.py new file mode 100644 index 0000000000..c8d670dc67 --- /dev/null +++ b/src/backend/database_models/seeders/organization_seed.py @@ -0,0 +1,42 @@ +from sqlalchemy import text +from sqlalchemy.orm import Session + +from backend.database_models import Organization + + +def seed_default_organization(op): + """ + Seed default organization. + """ + # Previously we would seed the default deployments and models here. We've changed this + # behaviour during a refactor of the deployments module so that deployments and models + # are inserted when they're first used. This solves an issue where seed data would + # sometimes be inserted with invalid config data. + + _ = Session(op.get_bind()) + + # Seed default organization + sql_command = text( + """ + INSERT INTO organizations ( + id, name, created_at, updated_at + ) + VALUES ( + :id, :name, now(), now() + ) + ON CONFLICT (id) DO NOTHING; + """ + ).bindparams( + id="default", + name="Default Organization", + ) + op.execute(sql_command) + + +def delete_default_organization(op): + """ + Delete default organization. + """ + session = Session(op.get_bind()) + session.query(Organization).filter_by(id="default").delete() + session.commit() diff --git a/src/backend/pytest_integration.ini b/src/backend/pytest_integration.ini index c686703e0c..2c593c116a 100644 --- a/src/backend/pytest_integration.ini +++ b/src/backend/pytest_integration.ini @@ -1,5 +1,6 @@ [pytest] env = - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres + DATABASE_URL=postgresql://postgres:postgres@localhost:5433 filterwarnings = ignore::UserWarning:pydantic.* + ignore::DeprecationWarning \ No newline at end of file diff --git a/src/backend/services/cache.py b/src/backend/services/cache.py index 698598caef..3d2b3ef404 100644 --- a/src/backend/services/cache.py +++ b/src/backend/services/cache.py @@ -25,7 +25,7 @@ def cache_put(key: str, value: Any) -> None: client = get_client() if isinstance(value, dict): - client.hmset(key, value) + client.hset(key, value) else: client.set(key, value) diff --git a/src/backend/services/deployment.py b/src/backend/services/deployment.py index 96de913bd9..aac7b77b6f 100644 --- a/src/backend/services/deployment.py +++ b/src/backend/services/deployment.py @@ -95,7 +95,7 @@ def get_deployment_definition_by_name(session: DBSessionDep, deployment_name: st # Creates deployment in DB if it doesn't exist if definition.name not in [d.name for d in deployment_crud.get_deployments(session)]: - create_db_deployment(session, definition) + definition = create_db_deployment(session, definition) return definition diff --git a/src/backend/services/request_validators.py b/src/backend/services/request_validators.py index e2da5d64b8..d42a31b15d 100644 --- a/src/backend/services/request_validators.py +++ b/src/backend/services/request_validators.py @@ -11,6 +11,7 @@ from backend.crud import model as model_crud from backend.crud import organization as organization_crud from backend.database_models.database import DBSessionDep +from backend.exceptions import DeploymentNotFoundError from backend.model_deployments.utils import class_name_validator from backend.services import deployment as deployment_service from backend.services.agent import validate_agent_exists @@ -217,7 +218,12 @@ async def validate_env_vars(session: DBSessionDep, request: Request): invalid_keys = [] deployment_id = unquote_plus(request.path_params.get("deployment_id")) - deployment = deployment_service.get_deployment_instance_by_id(session, deployment_id) + try: + deployment = deployment_service.get_deployment_instance_by_id(session, deployment_id) + except DeploymentNotFoundError: + raise HTTPException( + status_code=404, detail=f"Deployment {deployment_id} not found." + ) for key in env_vars: if key not in deployment.env_vars(): diff --git a/src/backend/tests/integration/conftest.py b/src/backend/tests/integration/conftest.py index d2207d04fc..fb8528249f 100644 --- a/src/backend/tests/integration/conftest.py +++ b/src/backend/tests/integration/conftest.py @@ -18,7 +18,7 @@ from backend.schemas.user import User from backend.tests.unit.factories import get_factory -DATABASE_URL = os.environ["DATABASE_URL"] +DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://postgres:postgres@localhost:5433") @pytest.fixture @@ -162,7 +162,6 @@ def deployment(session: Session) -> Deployment: deployment_class_name="CohereDeployment" ) - @pytest.fixture def model(session: Session) -> Model: return get_factory("Model", session).create() diff --git a/src/backend/tests/integration/routers/test_agent.py b/src/backend/tests/integration/routers/test_agent.py index e80c23842a..32b457d631 100644 --- a/src/backend/tests/integration/routers/test_agent.py +++ b/src/backend/tests/integration/routers/test_agent.py @@ -283,7 +283,8 @@ def test_create_agent_deployment_not_in_db( "deployment": CohereDeployment.name(), } cohere_deployment = deployment_crud.get_deployment_by_name(session, CohereDeployment.name()) - deployment_crud.delete_deployment(session, cohere_deployment.id) + if cohere_deployment: + deployment_crud.delete_deployment(session, cohere_deployment.id) response = session_client.post( "/v1/agents", json=request_json, headers={"User-Id": user.id} ) diff --git a/src/backend/tests/integration/routers/test_conversation.py b/src/backend/tests/integration/routers/test_conversation.py index 1700c7fd1e..7bd296ca38 100644 --- a/src/backend/tests/integration/routers/test_conversation.py +++ b/src/backend/tests/integration/routers/test_conversation.py @@ -11,6 +11,10 @@ from backend.tests.unit.factories import get_factory +@pytest.mark.skipif( + os.environ.get("COHERE_API_KEY") is None, + reason="Cohere API key not set, skipping test", +) def test_search_conversations( session_client: TestClient, session: Session, @@ -64,7 +68,10 @@ def test_search_conversations_with_reranking( assert len(results) == 1 assert results[0]["id"] == conversation2.id - +@pytest.mark.skipif( + os.environ.get("COHERE_API_KEY") is None, + reason="Cohere API key not set, skipping test", +) def test_search_conversations_no_conversations( session_client: TestClient, session: Session, diff --git a/src/backend/tests/integration/services/auth/__init__.py b/src/backend/tests/integration/services/auth/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/backend/tests/integration/services/auth/strategies/__init__.py b/src/backend/tests/integration/services/auth/strategies/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/backend/tests/unit/configuration.yaml b/src/backend/tests/unit/configuration.yaml index 6fa7a4e576..7213a15b14 100644 --- a/src/backend/tests/unit/configuration.yaml +++ b/src/backend/tests/unit/configuration.yaml @@ -1,5 +1,5 @@ deployments: - default_deployment: + default_deployment: cohere_platform enabled_deployments: sagemaker: access_key: "sagemaker_access_key" diff --git a/src/backend/tests/unit/conftest.py b/src/backend/tests/unit/conftest.py index 9b180aaef5..3c12fe33ea 100644 --- a/src/backend/tests/unit/conftest.py +++ b/src/backend/tests/unit/conftest.py @@ -15,12 +15,13 @@ from backend.database_models import get_session from backend.database_models.base import CustomFilterQuery +from backend.database_models.deployment import Deployment from backend.main import app, create_app from backend.schemas.organization import Organization from backend.schemas.user import User from backend.tests.unit.factories import get_factory -DATABASE_URL = os.environ["DATABASE_URL"] +DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://postgres:postgres@localhost:5433") MASTER_DB_NAME = "postgres" TEST_DB_PREFIX = "postgres_" MASTER_DATABASE_FULL_URL = f"{DATABASE_URL}/{MASTER_DB_NAME}" @@ -58,7 +59,7 @@ def client(): yield TestClient(app) -@pytest.fixture(scope="session") +@pytest.fixture def engine(worker_id: str) -> Generator[Any, None, None]: """ Yields a SQLAlchemy engine which is disposed of after the test session @@ -81,6 +82,32 @@ def engine(worker_id: str) -> Generator[Any, None, None]: drop_test_database_if_exists(test_db_name) +@pytest.fixture(scope="session") +def engine_chat(worker_id: str) -> Generator[Any, None, None]: + """ + Yields a SQLAlchemy engine which is disposed of after the test session + """ + test_db_name = f"{TEST_DB_PREFIX}{worker_id}" + if worker_id == "master": + test_db_name = f"{TEST_DB_PREFIX}{worker_id}_chat" + + test_db_url = f"{DATABASE_URL}/{test_db_name}" + + drop_test_database_if_exists(test_db_name) + create_test_database(test_db_name) + engine = create_engine(test_db_url, echo=True) + + with engine.begin(): + alembic_cfg = Config("src/backend/alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", test_db_url) + upgrade(alembic_cfg, "head") + + yield engine + + engine.dispose() + drop_test_database_if_exists(test_db_name) + + @pytest.fixture(scope="function") def session(engine: Any) -> Generator[Session, None, None]: """ @@ -122,7 +149,7 @@ def override_get_session() -> Generator[Session, Any, None]: @pytest.fixture(scope="session") -def session_chat(engine: Any) -> Generator[Session, None, None]: +def session_chat(engine_chat: Any) -> Generator[Session, None, None]: """ Yields a SQLAlchemy session within a transaction that is rolled back after every session @@ -130,7 +157,7 @@ def session_chat(engine: Any) -> Generator[Session, None, None]: We need to use the fixture in the session scope because the chat endpoint is asynchronous and needs to be open for the entire session """ - connection = engine.connect() + connection = engine_chat.connect() transaction = connection.begin() # Use connection within the started transaction session = Session(bind=connection, query_cls=CustomFilterQuery) @@ -188,6 +215,11 @@ def user(session: Session) -> User: def organization(session: Session) -> Organization: return get_factory("Organization", session).create() +@pytest.fixture +def deployment(session: Session) -> Deployment: + return get_factory("Deployment", session).create( + deployment_class_name="CohereDeployment" + ) @pytest.fixture def mock_available_model_deployments(request): diff --git a/src/backend/tests/integration/crud/test_deployment.py b/src/backend/tests/unit/crud/test_deployment.py similarity index 100% rename from src/backend/tests/integration/crud/test_deployment.py rename to src/backend/tests/unit/crud/test_deployment.py diff --git a/src/backend/tests/integration/crud/test_model.py b/src/backend/tests/unit/crud/test_model.py similarity index 100% rename from src/backend/tests/integration/crud/test_model.py rename to src/backend/tests/unit/crud/test_model.py diff --git a/src/backend/tests/integration/routers/test_chat.py b/src/backend/tests/unit/routers/test_chat.py similarity index 100% rename from src/backend/tests/integration/routers/test_chat.py rename to src/backend/tests/unit/routers/test_chat.py diff --git a/src/backend/tests/integration/routers/test_deployment.py b/src/backend/tests/unit/routers/test_deployment.py similarity index 98% rename from src/backend/tests/integration/routers/test_deployment.py rename to src/backend/tests/unit/routers/test_deployment.py index 6df4d29a1a..2d1ebbbd69 100644 --- a/src/backend/tests/integration/routers/test_deployment.py +++ b/src/backend/tests/unit/routers/test_deployment.py @@ -157,9 +157,9 @@ def test_set_env_vars( def test_set_env_vars_with_invalid_deployment_name( - client: TestClient + session_client: TestClient ): - response = client.post("/v1/deployments/unknown/update_config", json={}) + response = session_client.post("/v1/deployments/unknown/update_config", json={}) assert response.status_code == 404 diff --git a/src/backend/tests/integration/routers/test_model.py b/src/backend/tests/unit/routers/test_model.py similarity index 100% rename from src/backend/tests/integration/routers/test_model.py rename to src/backend/tests/unit/routers/test_model.py diff --git a/src/backend/tests/integration/crud/__init__.py b/src/backend/tests/unit/services/auth/__init__.py similarity index 100% rename from src/backend/tests/integration/crud/__init__.py rename to src/backend/tests/unit/services/auth/__init__.py diff --git a/src/backend/tests/integration/services/__init__.py b/src/backend/tests/unit/services/auth/strategies/__init__.py similarity index 100% rename from src/backend/tests/integration/services/__init__.py rename to src/backend/tests/unit/services/auth/strategies/__init__.py diff --git a/src/backend/tests/integration/services/auth/strategies/test_basic.py b/src/backend/tests/unit/services/auth/strategies/test_basic.py similarity index 100% rename from src/backend/tests/integration/services/auth/strategies/test_basic.py rename to src/backend/tests/unit/services/auth/strategies/test_basic.py diff --git a/src/backend/tests/integration/services/auth/test_jwt.py b/src/backend/tests/unit/services/auth/test_jwt.py similarity index 100% rename from src/backend/tests/integration/services/auth/test_jwt.py rename to src/backend/tests/unit/services/auth/test_jwt.py diff --git a/src/backend/tests/integration/services/auth/test_request_validators.py b/src/backend/tests/unit/services/auth/test_request_validators.py similarity index 100% rename from src/backend/tests/integration/services/auth/test_request_validators.py rename to src/backend/tests/unit/services/auth/test_request_validators.py diff --git a/src/backend/tests/integration/services/test_cache.py b/src/backend/tests/unit/services/test_cache.py similarity index 100% rename from src/backend/tests/integration/services/test_cache.py rename to src/backend/tests/unit/services/test_cache.py diff --git a/src/backend/tests/unit/services/test_deployment.py b/src/backend/tests/unit/services/test_deployment.py index 9081706385..d7eae458c0 100644 --- a/src/backend/tests/unit/services/test_deployment.py +++ b/src/backend/tests/unit/services/test_deployment.py @@ -83,7 +83,12 @@ def test_get_deployment_definition_by_name(session, mock_available_model_deploym def test_get_deployment_definition_by_name_no_db_deployments(session, mock_available_model_deployments, clear_db_deployments) -> None: definition = deployment_service.get_deployment_definition_by_name(session, MockCohereDeployment.name()) - assert definition == MockCohereDeployment.to_deployment_definition() + mock = MockCohereDeployment.to_deployment_definition() + assert definition.name == mock.name + assert definition.models == mock.models + assert definition.class_name == mock.class_name + assert definition.config == mock.config + def test_get_deployment_definition_by_name_wrong_name(session, mock_available_model_deployments) -> None: with pytest.raises(DeploymentNotFoundError): diff --git a/src/interfaces/slack_bot/package-lock.json b/src/interfaces/slack_bot/package-lock.json index 65fa12a530..0ff14cb010 100644 --- a/src/interfaces/slack_bot/package-lock.json +++ b/src/interfaces/slack_bot/package-lock.json @@ -29,7 +29,7 @@ "slackify-markdown": "^4.4.0", "tsup": "^8.0.0", "typescript": "^5.4.5", - "vite": "^5.4.6", + "vite": "^5.4.14", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0", "word-extractor": "^1.0.4" @@ -7609,9 +7609,10 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/interfaces/slack_bot/package.json b/src/interfaces/slack_bot/package.json index 380111e036..a1efc666ef 100644 --- a/src/interfaces/slack_bot/package.json +++ b/src/interfaces/slack_bot/package.json @@ -43,7 +43,7 @@ "slackify-markdown": "^4.4.0", "tsup": "^8.0.0", "typescript": "^5.4.5", - "vite": "^5.4.6", + "vite": "^5.4.14", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0", "word-extractor": "^1.0.4"