From 5a8d313e2530d1ee647acf2c14299ba0d3a5e2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <571533+jrmi@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:13:03 +0100 Subject: [PATCH 1/6] Fix form data init inside form elements (#4272) --- web-frontend/modules/builder/store/page.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web-frontend/modules/builder/store/page.js b/web-frontend/modules/builder/store/page.js index be9b07473c..1425bd3cb0 100644 --- a/web-frontend/modules/builder/store/page.js +++ b/web-frontend/modules/builder/store/page.js @@ -18,6 +18,7 @@ export function populatePage(page) { workflowActions: [], elementTree: [], contents: null, + formData: {}, } } From 393f4bb36f1475ae2df54a57b8beb45ba00dc1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <571533+jrmi@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:30:56 +0100 Subject: [PATCH 2/6] Fix table button collection field loading state (#4273) --- ..._fix_synchronised_button_loading_state_in_tables.json | 9 +++++++++ .../components/elements/components/TableElement.vue | 1 + web-frontend/modules/builder/elementTypes.js | 9 +++++++-- web-frontend/modules/builder/mixins/collectionElement.js | 2 ++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 changelog/entries/unreleased/bug/4268_fix_synchronised_button_loading_state_in_tables.json diff --git a/changelog/entries/unreleased/bug/4268_fix_synchronised_button_loading_state_in_tables.json b/changelog/entries/unreleased/bug/4268_fix_synchronised_button_loading_state_in_tables.json new file mode 100644 index 0000000000..865bb14a03 --- /dev/null +++ b/changelog/entries/unreleased/bug/4268_fix_synchronised_button_loading_state_in_tables.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fix synchronised button loading state in tables", + "issue_origin": "github", + "issue_number": 4268, + "domain": "builder", + "bullet_points": [], + "created_at": "2025-11-17" +} \ No newline at end of file diff --git a/web-frontend/modules/builder/components/elements/components/TableElement.vue b/web-frontend/modules/builder/components/elements/components/TableElement.vue index c83099d73e..44c02f3c3f 100644 --- a/web-frontend/modules/builder/components/elements/components/TableElement.vue +++ b/web-frontend/modules/builder/components/elements/components/TableElement.vue @@ -41,6 +41,7 @@ row, rowIndex, field, + allowSameElement: true, }) " v-bind="value" diff --git a/web-frontend/modules/builder/elementTypes.js b/web-frontend/modules/builder/elementTypes.js index 2db77681f2..291c515531 100644 --- a/web-frontend/modules/builder/elementTypes.js +++ b/web-frontend/modules/builder/elementTypes.js @@ -782,7 +782,12 @@ export class ElementType extends Registerable { * */ uniqueElementId({ element, applicationContext }) { - const { recordIndexPath, builder, page } = applicationContext + const { + recordIndexPath, + builder, + page, + allowSameElement = false, + } = applicationContext const elementPage = element.page_id === page.id @@ -792,7 +797,7 @@ export class ElementType extends Registerable { const collectionAncestorLength = this.getCollectionAncestry({ page: elementPage, element, - allowSameElement: false, + allowSameElement, }).length // We might be asking the uniqueID for an outside element so we don't diff --git a/web-frontend/modules/builder/mixins/collectionElement.js b/web-frontend/modules/builder/mixins/collectionElement.js index a245daf057..3b06aba039 100644 --- a/web-frontend/modules/builder/mixins/collectionElement.js +++ b/web-frontend/modules/builder/mixins/collectionElement.js @@ -198,9 +198,11 @@ export default { row, rowIndex, field = null, + ...rest }) { const newApplicationContext = { recordIndexPath: [...applicationContext.recordIndexPath, rowIndex], + ...rest, } if (field) { newApplicationContext.field = field From eefb7bdaf66f5023f02979b952cfc2246c2318fc Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 17 Nov 2025 18:20:01 +0100 Subject: [PATCH 3/6] Add GitHub action jobs to publish release (#4270) --- .github/workflows/ci.yml | 54 ++-- .github/workflows/publish-release-images.yml | 318 +++++++++++++++++++ backend/Dockerfile | 5 +- web-frontend/Dockerfile | 17 +- 4 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/publish-release-images.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 301cbd89d9..3eeafddd67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ env: IMAGE_REPO: ${{ github.repository }} CI_IMAGE_TAG_PREFIX: ci- DEVELOP_BRANCH_NAME: develop + REAL_GITHUB_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} jobs: check-build-and-publish: @@ -77,7 +78,7 @@ jobs: id: image run: | IMAGE_NAME="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend_dev" - IMAGE_TAG="${{ env.CI_IMAGE_TAG_PREFIX }}${{ github.sha }}" + IMAGE_TAG="${{ env.CI_IMAGE_TAG_PREFIX }}${{ env.REAL_GITHUB_SHA }}" FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" echo "full=${FULL_IMAGE}" >> $GITHUB_OUTPUT @@ -109,7 +110,7 @@ jobs: cache-to: type=inline labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ env.REAL_GITHUB_SHA }} org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} build-frontend: @@ -138,7 +139,7 @@ jobs: id: image run: | IMAGE_NAME="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend_dev" - IMAGE_TAG="${{ env.CI_IMAGE_TAG_PREFIX }}${{ github.sha }}" + IMAGE_TAG="${{ env.CI_IMAGE_TAG_PREFIX }}${{ env.REAL_GITHUB_SHA }}" FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" echo "full=${FULL_IMAGE}" >> $GITHUB_OUTPUT @@ -170,7 +171,7 @@ jobs: cache-to: type=inline labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ env.REAL_GITHUB_SHA }} org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} # ========================================================================== @@ -609,6 +610,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest needs: + - detect-changes - build-backend - build-frontend - backend-lint @@ -910,12 +912,12 @@ jobs: file: backend/Dockerfile push: true tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ github.sha }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend_dev:ci-${{ github.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ env.REAL_GITHUB_SHA }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend_dev:ci-${{ env.REAL_GITHUB_SHA }} cache-to: type=inline labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ env.REAL_GITHUB_SHA }} org.opencontainers.image.title=backend org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} @@ -949,12 +951,12 @@ jobs: file: web-frontend/Dockerfile push: true tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ github.sha }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend_dev:ci-${{ github.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ env.REAL_GITHUB_SHA }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend_dev:ci-${{ env.REAL_GITHUB_SHA }} cache-to: type=inline labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ env.REAL_GITHUB_SHA }} org.opencontainers.image.title=web-frontend org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} @@ -987,14 +989,14 @@ jobs: file: deploy/all-in-one/Dockerfile push: true build-args: | - FROM_BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ github.sha }} - FROM_WEBFRONTEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ github.sha }} + FROM_BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ env.REAL_GITHUB_SHA }} + FROM_WEBFRONTEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ env.REAL_GITHUB_SHA }} tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ github.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ env.REAL_GITHUB_SHA }} cache-to: type=inline labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ env.REAL_GITHUB_SHA }} org.opencontainers.image.title=baserow org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} @@ -1026,13 +1028,13 @@ jobs: file: deploy/cloudron/Dockerfile push: true build-args: | - FROM_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ github.sha }} + FROM_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ env.REAL_GITHUB_SHA }} tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:ci-tested-${{ github.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:ci-tested-${{ env.REAL_GITHUB_SHA }} cache-to: type=inline labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ env.REAL_GITHUB_SHA }} org.opencontainers.image.title=cloudron org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} @@ -1071,7 +1073,7 @@ jobs: - name: Create and push develop-latest image on Docker Hub run: | - SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ github.sha }} + SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ env.REAL_GITHUB_SHA }} TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/backend:develop-latest echo "Publishing $SOURCE → $TARGET" docker buildx imagetools create -t $TARGET $SOURCE @@ -1111,7 +1113,7 @@ jobs: - name: Create and push develop-latest image on Docker Hub run: | - SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ github.sha }} + SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ env.REAL_GITHUB_SHA }} TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/web-frontend:develop-latest echo "Publishing $SOURCE → $TARGET" docker buildx imagetools create -t $TARGET $SOURCE @@ -1151,7 +1153,7 @@ jobs: - name: Create and push develop-latest image on Docker Hub run: | - SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ github.sha }} + SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ env.REAL_GITHUB_SHA }} TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/baserow:develop-latest echo "Publishing $SOURCE → $TARGET" docker buildx imagetools create -t $TARGET $SOURCE @@ -1191,7 +1193,7 @@ jobs: - name: Create and push develop-latest image on Docker Hub run: | - SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:ci-tested-${{ github.sha }} + SOURCE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:ci-tested-${{ env.REAL_GITHUB_SHA }} TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/cloudron:develop-latest echo "Publishing $SOURCE → $TARGET" docker buildx imagetools create -t $TARGET $SOURCE @@ -1224,11 +1226,11 @@ jobs: run: | echo "🧩 Generating updated image references..." - BACKEND_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ github.sha }}" - BACKEND_DEV_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend_dev:ci-${{ github.sha }}" - WEB_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ github.sha }}" - WEB_DEV_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend_dev:ci-${{ github.sha }}" - ALL_IN_ONE_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ github.sha }}" + BACKEND_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:ci-tested-${{ env.REAL_GITHUB_SHA }}" + BACKEND_DEV_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend_dev:ci-${{ env.REAL_GITHUB_SHA }}" + WEB_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:ci-tested-${{ env.REAL_GITHUB_SHA }}" + WEB_DEV_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend_dev:ci-${{ env.REAL_GITHUB_SHA }}" + ALL_IN_ONE_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:ci-tested-${{ env.REAL_GITHUB_SHA }}" echo "$BACKEND_IMAGE" > plugins/saas/backend/build_from_image.version echo "$BACKEND_DEV_IMAGE" > plugins/saas/backend/build_from_dev_image.version diff --git a/.github/workflows/publish-release-images.yml b/.github/workflows/publish-release-images.yml new file mode 100644 index 0000000000..c27980680c --- /dev/null +++ b/.github/workflows/publish-release-images.yml @@ -0,0 +1,318 @@ +name: Publish Release Images + +on: + push: + tags: + - "*" + +env: + REGISTRY: ghcr.io + IMAGE_REPO: ${{ github.repository }} + RELEASE_DOCKER_REGISTRY: ${{ secrets.RELEASE_DOCKER_REGISTRY }} + RELEASE_DOCKER_REPOSITORY: ${{ secrets.RELEASE_DOCKER_REPOSITORY }} + RELEASE_DOCKER_USERNAME: ${{ secrets.RELEASE_DOCKER_USERNAME }} + RELEASE_DOCKER_PASSWORD: ${{ secrets.RELEASE_DOCKER_PASSWORD }} + CI_TESTED_TAG_SUFFIX: ci-tested-${{ github.sha }} + +jobs: + build-backend-arm64-release-image: + name: Build backend arm64 image (GHCR) + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + steps: + - name: Checkout code at tag + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push backend arm64 image to GHCR + uses: docker/build-push-action@v5 + with: + context: . + file: backend/Dockerfile + push: true + platforms: linux/arm64 + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + cache-from: | + type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:${{ env.CI_TESTED_TAG_SUFFIX }} + cache-to: type=inline + + publish-backend-release-tagged-image: + name: Publish backend:${{ github.ref_name }} (multi-arch) + runs-on: ubuntu-latest + needs: + - build-backend-arm64-release-image + permissions: + contents: read + packages: read + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Release Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.RELEASE_DOCKER_REGISTRY }} + username: ${{ env.RELEASE_DOCKER_USERNAME }} + password: ${{ env.RELEASE_DOCKER_PASSWORD }} + + - name: Create multi-arch backend:${{ github.ref_name }} in release registry + run: | + AMD64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:${{ env.CI_TESTED_TAG_SUFFIX }} + ARM64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/backend:${{ github.ref_name }} + + echo "Creating multi-arch image:" + echo " $TARGET" + echo "from:" + echo " $AMD64" + echo " $ARM64" + + docker buildx imagetools create -t "$TARGET" "$AMD64" "$ARM64" + + build-webfrontend-arm64-release-image: + name: Build web-frontend arm64 image (GHCR) + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + steps: + - name: Checkout code at tag + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push web-frontend arm64 image to GHCR + uses: docker/build-push-action@v5 + with: + context: . + file: web-frontend/Dockerfile + push: true + platforms: linux/arm64 + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + cache-from: | + type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:${{ env.CI_TESTED_TAG_SUFFIX }} + cache-to: type=inline + + publish-webfrontend-release-tagged-image: + name: Publish web-frontend:${{ github.ref_name }} (multi-arch) + runs-on: ubuntu-latest + needs: + - build-webfrontend-arm64-release-image + permissions: + contents: read + packages: read + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Release Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.RELEASE_DOCKER_REGISTRY }} + username: ${{ env.RELEASE_DOCKER_USERNAME }} + password: ${{ env.RELEASE_DOCKER_PASSWORD }} + + - name: Create multi-arch web-frontend:${{ github.ref_name }} in release registry + run: | + AMD64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:${{ env.CI_TESTED_TAG_SUFFIX }} + ARM64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/web-frontend:${{ github.ref_name }} + + echo "Creating multi-arch image:" + echo " $TARGET" + echo "from:" + echo " $AMD64" + echo " $ARM64" + + docker buildx imagetools create --debug -t "$TARGET" "$AMD64" "$ARM64" + + build-all-in-one-arm64-release-image: + name: Build baserow (all-in-one) arm64 image (GHCR) + runs-on: ubuntu-24.04-arm + needs: + - build-backend-arm64-release-image + - build-webfrontend-arm64-release-image + permissions: + contents: read + packages: write + steps: + - name: Checkout code at tag + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push baserow arm64 image to GHCR + uses: docker/build-push-action@v5 + with: + context: . + file: deploy/all-in-one/Dockerfile + push: true + platforms: linux/arm64 + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + build-args: | + FROM_BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/backend:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + FROM_WEBFRONTEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/web-frontend:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + cache-from: | + type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:${{ env.CI_TESTED_TAG_SUFFIX }} + cache-to: type=inline + + publish-all-in-one-release-tagged-image: + name: Publish baserow:${{ github.ref_name }} (multi-arch) + runs-on: ubuntu-latest + needs: + - build-all-in-one-arm64-release-image + permissions: + contents: read + packages: read + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Release Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.RELEASE_DOCKER_REGISTRY }} + username: ${{ env.RELEASE_DOCKER_USERNAME }} + password: ${{ env.RELEASE_DOCKER_PASSWORD }} + + - name: Create multi-arch baserow:${{ github.ref_name }} in release registry + run: | + AMD64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:${{ env.CI_TESTED_TAG_SUFFIX }} + ARM64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/baserow:${{ github.ref_name }} + + echo "Creating multi-arch image:" + echo " $TARGET" + echo "from:" + echo " $AMD64" + echo " $ARM64" + + docker buildx imagetools create -t "$TARGET" "$AMD64" "$ARM64" + + build-cloudron-arm64-release-image: + name: Build cloudron arm64 image (GHCR) + runs-on: ubuntu-24.04-arm + needs: + - build-all-in-one-arm64-release-image + permissions: + contents: read + packages: write + steps: + - name: Checkout code at tag + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push cloudron arm64 image to GHCR + uses: docker/build-push-action@v5 + with: + context: . + file: deploy/cloudron/Dockerfile + push: true + platforms: linux/arm64 + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + build-args: | + FROM_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/baserow:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + cache-from: | + type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:${{ env.CI_TESTED_TAG_SUFFIX }} + cache-to: type=inline + + publish-cloudron-release-tagged-image: + name: Publish cloudron:${{ github.ref_name }} (multi-arch) + runs-on: ubuntu-latest + needs: + - build-cloudron-arm64-release-image + permissions: + contents: read + packages: read + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Release Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.RELEASE_DOCKER_REGISTRY }} + username: ${{ env.RELEASE_DOCKER_USERNAME }} + password: ${{ env.RELEASE_DOCKER_PASSWORD }} + + - name: Create multi-arch cloudron:${{ github.ref_name }} in release registry + run: | + AMD64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:${{ env.CI_TESTED_TAG_SUFFIX }} + ARM64=${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/cloudron:${{ env.CI_TESTED_TAG_SUFFIX }}-arm64 + TARGET=${{ env.RELEASE_DOCKER_REPOSITORY }}/cloudron:${{ github.ref_name }} + + echo "Creating multi-arch image:" + echo " $TARGET" + echo "from:" + echo " $AMD64" + echo " $ARM64" + + docker buildx imagetools create -t "$TARGET" "$AMD64" "$ARM64" diff --git a/backend/Dockerfile b/backend/Dockerfile index cba144ada6..a1aed0b6fb 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -70,7 +70,10 @@ ENV PIP_CACHE_DIR=/tmp/baserow_pip_cache # hadolint ignore=SC1091,DL3042,DL3013 RUN --mount=type=cache,mode=777,target=$PIP_CACHE_DIR,uid=$UID,gid=$GID . /baserow/venv/bin/activate \ && pip3 install --no-cache-dir --upgrade pip setuptools==78.1.1 \ - && pip3 install -r /baserow/requirements/base.txt + && pip3 install --no-compile -r /baserow/requirements/base.txt \ + && find /baserow/venv -type d -name "__pycache__" -prune -exec rm -rf '{}' + \ + && find /baserow/venv -type f \( -name "*.pyc" -o -name "*.pyo" \) -delete \ + && find /baserow/venv -type f \( -name "*.c" -o -name "*.h" \) -delete # Build a dev_deps stage which also has the dev dependencies for use by the dev layer. FROM base AS dev_deps diff --git a/web-frontend/Dockerfile b/web-frontend/Dockerfile index ce55073fdb..d08ff63d7a 100644 --- a/web-frontend/Dockerfile +++ b/web-frontend/Dockerfile @@ -44,9 +44,6 @@ COPY --chown=$UID:$GID ./web-frontend/package.json ./web-frontend/yarn.lock /bas WORKDIR /baserow/web-frontend -# We still need dev-dependencies as we will be running a nuxt build below -RUN yarn install && yarn cache clean - COPY --chown=$UID:$GID ./web-frontend /baserow/web-frontend/ COPY --chown=$UID:$GID ./premium/web-frontend /baserow/premium/web-frontend/ COPY --chown=$UID:$GID ./enterprise/web-frontend /baserow/enterprise/web-frontend/ @@ -63,6 +60,11 @@ EXPOSE 3000 FROM base AS dev +WORKDIR /baserow/web-frontend + +# Install dev dependencies for running nuxt dev + tests. +RUN yarn install && yarn cache clean + COPY --chown=$UID:$GID ./tests /baserow/tests/ # Create symlinks for jest tests @@ -76,6 +78,13 @@ CMD ["nuxt-dev"] FROM base AS local # Run the nuxt build and then remove all dev dependencies as we don't need them after. -RUN yarn run build-local && rm -rf node_modules && yarn install --production && yarn cache clean +RUN yarn install \ + && yarn run build-local \ + && rm -rf node_modules \ + && yarn install --production \ + && yarn cache clean \ + && find node_modules -type f \( -name "*.o" -o -name "*.a" \) -delete \ + && find node_modules -type d \( -name "test" -o -name "tests" -o -name "__tests__" -o -name "example" -o -name "examples" -o -name "doc" -o -name "docs" \) -prune -exec rm -rf '{}' + \ + && find .nuxt -type f -name "*.map" -delete CMD ["nuxt-local"] From e22bf6fc1475367decfac39499e749d47090e359 Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:00:29 +0100 Subject: [PATCH 4/6] Fix kuma history bug (#4269) * Reduce loaded tools to reduce context; improved history summarizer * Fix tests * Added router * Simplified router * Address copilot feedback * Fixed tool descriptions; added temperature parameter for the assistant * Added translations * Improve description for workflow/automation tool loader * double-check sources are strings * address Peter's feedback * remove FF; added changelog entry --- .env.example | 1 + backend/requirements/base.in | 2 +- backend/requirements/base.txt | 2 +- .../builder/locale/en/LC_MESSAGES/django.po | 4 +- backend/src/baserow/core/feature_flags.py | 1 - .../core/locale/en/LC_MESSAGES/django.po | 23 +- .../baserow/locale/en/LC_MESSAGES/django.po | 48 +-- .../introduce_kuma_baserow_ai_assistant.json | 9 + docker-compose.local-build.yml | 1 + docker-compose.no-caddy.yml | 1 + docker-compose.yml | 1 + docs/installation/ai-assistant.md | 9 + .../baserow_enterprise/api/assistant/views.py | 18 +- .../backend/src/baserow_enterprise/apps.py | 22 +- .../baserow_enterprise/assistant/assistant.py | 397 +++++++++++------- .../baserow_enterprise/assistant/prompts.py | 146 ++++++- .../assistant/signatures.py | 67 +++ .../assistant/tools/__init__.py | 2 +- .../assistant/tools/automation/__init__.py | 4 +- .../assistant/tools/automation/tools.py | 44 +- .../assistant/tools/database/tools.py | 272 ++++++------ .../assistant/tools/database/types/table.py | 11 +- .../assistant/tools/database/types/views.py | 37 +- .../assistant/tools/database/utils.py | 4 +- .../assistant/tools/search_docs/tools.py | 103 ----- .../__init__.py | 0 .../handler.py | 14 +- .../assistant/tools/search_user_docs/tools.py | 156 +++++++ .../src/baserow_enterprise/assistant/types.py | 3 + .../config/settings/settings.py | 3 + .../locale/en/LC_MESSAGES/django.po | 101 +++-- .../commands/sync_knowledge_base.py | 4 +- .../api/assistant/test_assistant_views.py | 84 +++- .../assistant/test_assistant.py | 329 +++++++++++---- ...est_assistant_automation_workflow_tools.py | 152 ++++++- .../test_assistant_database_rows_tools.py | 12 +- .../test_assistant_database_table_tools.py | 55 ++- .../test_assistant_database_views_tools.py | 183 +++++--- ...t_assistant_knowledge_retrieval_handler.py | 27 +- .../assistant/test_sync_knowledge_base.py | 4 +- .../modules/baserow_enterprise/plugins.js | 9 +- .../modules/core/plugins/featureFlags.js | 1 - 42 files changed, 1644 insertions(+), 722 deletions(-) create mode 100644 changelog/entries/unreleased/feature/introduce_kuma_baserow_ai_assistant.json create mode 100644 enterprise/backend/src/baserow_enterprise/assistant/signatures.py delete mode 100644 enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/tools.py rename enterprise/backend/src/baserow_enterprise/assistant/tools/{search_docs => search_user_docs}/__init__.py (100%) rename enterprise/backend/src/baserow_enterprise/assistant/tools/{search_docs => search_user_docs}/handler.py (97%) create mode 100644 enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/tools.py diff --git a/.env.example b/.env.example index 391f0c463d..76f11bcd45 100644 --- a/.env.example +++ b/.env.example @@ -178,3 +178,4 @@ DATABASE_NAME=baserow # BASEROW_EMBEDDINGS_API_URL=http://embeddings # BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL=groq/openai/gpt-oss-120b # Needs GROQ_API_KEY env var set too +# BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE=0.3 diff --git a/backend/requirements/base.in b/backend/requirements/base.in index 356bcc0f99..8ae549aae6 100644 --- a/backend/requirements/base.in +++ b/backend/requirements/base.in @@ -88,4 +88,4 @@ httpcore==1.0.9 # Pinned to address vulnerability. genson==1.3.0 pyotp==2.9.0 qrcode==8.2 -udspy==0.1.6 +udspy==0.1.7 diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index 53b51bb15a..bb595f63e4 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -675,7 +675,7 @@ tzdata==2025.2 # -r base.in # django-celery-beat # kombu -udspy==0.1.6 +udspy==0.1.7 # via -r base.in unicodecsv==0.14.1 # via -r base.in diff --git a/backend/src/baserow/contrib/builder/locale/en/LC_MESSAGES/django.po b/backend/src/baserow/contrib/builder/locale/en/LC_MESSAGES/django.po index 7a00f027a4..717d594c98 100644 --- a/backend/src/baserow/contrib/builder/locale/en/LC_MESSAGES/django.po +++ b/backend/src/baserow/contrib/builder/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-08-06 12:24+0000\n" +"POT-Creation-Date: 2025-11-17 15:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -46,7 +46,7 @@ msgstr "" msgid "Last name" msgstr "" -#: src/baserow/contrib/builder/data_providers/data_provider_types.py:605 +#: src/baserow/contrib/builder/data_providers/data_provider_types.py:619 #, python-format msgid "%(user_source_name)s member" msgstr "" diff --git a/backend/src/baserow/core/feature_flags.py b/backend/src/baserow/core/feature_flags.py index 06a6918236..a1f79bbd80 100644 --- a/backend/src/baserow/core/feature_flags.py +++ b/backend/src/baserow/core/feature_flags.py @@ -2,7 +2,6 @@ from baserow.core.exceptions import FeatureDisabledException -FF_ASSISTANT = "assistant" FF_WORKSPACE_SEARCH = "workspace-search" FF_DATE_DEPENDENCY = "date_dependency" FF_ENABLE_ALL = "*" diff --git a/backend/src/baserow/core/locale/en/LC_MESSAGES/django.po b/backend/src/baserow/core/locale/en/LC_MESSAGES/django.po index 8c5f145c01..3c42d63ebd 100644 --- a/backend/src/baserow/core/locale/en/LC_MESSAGES/django.po +++ b/backend/src/baserow/core/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-13 19:58+0000\n" +"POT-Creation-Date: 2025-11-17 15:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -468,6 +468,27 @@ msgid "" "Item of type \"%(item_type)s\" (%(item_id)s) has been restored from trash" msgstr "" +#: src/baserow/core/two_factor_auth/actions.py:19 +msgid "Configure two-factor authentication" +msgstr "" + +#: src/baserow/core/two_factor_auth/actions.py:21 +#, python-format +msgid "" +"User \"%(user_email)s\" (%(user_id)s) configured %(provider_type)s (enabled " +"%(is_enabled)s) two-factor authentication." +msgstr "" + +#: src/baserow/core/two_factor_auth/actions.py:71 +msgid "Disable two-factor authentication" +msgstr "" + +#: src/baserow/core/two_factor_auth/actions.py:72 +#, python-format +msgid "" +"User \"%(user_email)s\" (%(user_id)s) disabled two-factor authentication." +msgstr "" + #: src/baserow/core/user/actions.py:25 msgid "Create User" msgstr "" diff --git a/backend/src/baserow/locale/en/LC_MESSAGES/django.po b/backend/src/baserow/locale/en/LC_MESSAGES/django.po index 72129cf0b9..67b9120a1e 100755 --- a/backend/src/baserow/locale/en/LC_MESSAGES/django.po +++ b/backend/src/baserow/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-13 19:58+0000\n" +"POT-Creation-Date: 2025-11-17 15:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,71 +54,63 @@ msgstr "" msgid "Local Baserow" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:32 +#: src/baserow/contrib/automation/nodes/actions.py:25 msgid "Create automation node" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:33 +#: src/baserow/contrib/automation/nodes/actions.py:26 #, python-format msgid "Node (%(node_id)s) created" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:104 +#: src/baserow/contrib/automation/nodes/actions.py:90 msgid "Update automation node" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:105 +#: src/baserow/contrib/automation/nodes/actions.py:91 #, python-format msgid "Node (%(node_id)s) updated" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:173 +#: src/baserow/contrib/automation/nodes/actions.py:159 msgid "Delete automation node" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:174 +#: src/baserow/contrib/automation/nodes/actions.py:160 #, python-format msgid "Node (%(node_id)s) deleted" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:231 -msgid "Order nodes" -msgstr "" - -#: src/baserow/contrib/automation/nodes/actions.py:232 -msgid "Node order changed" -msgstr "" - -#: src/baserow/contrib/automation/nodes/actions.py:300 +#: src/baserow/contrib/automation/nodes/actions.py:217 msgid "Duplicate automation node" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:301 +#: src/baserow/contrib/automation/nodes/actions.py:218 #, python-format msgid "Node (%(node_id)s) duplicated" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:384 +#: src/baserow/contrib/automation/nodes/actions.py:289 msgid "Replace automation node" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:386 +#: src/baserow/contrib/automation/nodes/actions.py:291 #, python-format msgid "" "Node (%(node_id)s) changed from a type of %(original_node_type)s to " "%(node_type)s" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:491 +#: src/baserow/contrib/automation/nodes/actions.py:377 msgid "Moved automation node" msgstr "" -#: src/baserow/contrib/automation/nodes/actions.py:492 +#: src/baserow/contrib/automation/nodes/actions.py:378 #, python-format msgid "Node (%(node_id)s) moved" msgstr "" -#: src/baserow/contrib/automation/nodes/node_types.py:176 +#: src/baserow/contrib/automation/nodes/node_types.py:260 msgid "Branch" msgstr "" @@ -213,18 +205,22 @@ msgstr "" msgid "Widget \"%(widget_title)s\" (%(widget_id)s) deleted" msgstr "" -#: src/baserow/contrib/integrations/core/service_types.py:1103 +#: src/baserow/contrib/integrations/core/service_types.py:1067 msgid "Branch taken" msgstr "" -#: src/baserow/contrib/integrations/core/service_types.py:1108 +#: src/baserow/contrib/integrations/core/service_types.py:1072 msgid "Label" msgstr "" -#: src/baserow/contrib/integrations/core/service_types.py:1110 +#: src/baserow/contrib/integrations/core/service_types.py:1074 msgid "The label of the branch that matched the condition." msgstr "" -#: src/baserow/contrib/integrations/core/service_types.py:1402 +#: src/baserow/contrib/integrations/core/service_types.py:1418 msgid "Triggered at" msgstr "" + +#: src/baserow/contrib/integrations/slack/service_types.py:166 +msgid "OK" +msgstr "" diff --git a/changelog/entries/unreleased/feature/introduce_kuma_baserow_ai_assistant.json b/changelog/entries/unreleased/feature/introduce_kuma_baserow_ai_assistant.json new file mode 100644 index 0000000000..c8a9ddc084 --- /dev/null +++ b/changelog/entries/unreleased/feature/introduce_kuma_baserow_ai_assistant.json @@ -0,0 +1,9 @@ +{ + "type": "feature", + "message": "Introduced Kuma, an AI-powered assistant to help you manage your workspace.", + "issue_origin": "github", + "issue_number": 3676, + "domain": "core", + "bullet_points": [], + "created_at": "2025-11-17" +} diff --git a/docker-compose.local-build.yml b/docker-compose.local-build.yml index 08c1325cb4..f72d660393 100644 --- a/docker-compose.local-build.yml +++ b/docker-compose.local-build.yml @@ -167,6 +167,7 @@ x-backend-variables: BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES: BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_AGG_BUCKETS: BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL: + BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE: BASEROW_EMBEDDINGS_API_URL: services: diff --git a/docker-compose.no-caddy.yml b/docker-compose.no-caddy.yml index 85a8bdda70..6ee987c896 100644 --- a/docker-compose.no-caddy.yml +++ b/docker-compose.no-caddy.yml @@ -185,6 +185,7 @@ x-backend-variables: BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES: BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_AGG_BUCKETS: BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL: + BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE: BASEROW_EMBEDDINGS_API_URL: services: diff --git a/docker-compose.yml b/docker-compose.yml index 2006a8d005..34c4374a9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -234,6 +234,7 @@ x-backend-variables: BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES: BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_AGG_BUCKETS: BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL: + BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE: BASEROW_EMBEDDINGS_API_URL: BASEROW_OAUTH_BACKEND_URL: BASEROW_TOTP_ISSUER_NAME: diff --git a/docs/installation/ai-assistant.md b/docs/installation/ai-assistant.md index 9a24ce89a3..e5c7be4676 100644 --- a/docs/installation/ai-assistant.md +++ b/docs/installation/ai-assistant.md @@ -21,8 +21,17 @@ Set the model you want, restart Baserow, and let migrations run. # Required BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL=openai/gpt-5-mini OPENAI_API_KEY=your_api_key + +# Optional - adjust LLM temperature (default: 0) +BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE=0 ``` +**About temperature:** +- Controls randomness in LLM responses (0.0 to 2.0) +- **Default: 0** (deterministic, consistent responses - recommended for production) +- Higher values (e.g., 0.7-1.0) = more creative/varied responses +- Lower values (e.g., 0-0.3) = more focused/consistent responses + ## 3) Provider presets Choose **one** provider block and set its variables. diff --git a/enterprise/backend/src/baserow_enterprise/api/assistant/views.py b/enterprise/backend/src/baserow_enterprise/api/assistant/views.py index e1b50cee76..1987e6f224 100644 --- a/enterprise/backend/src/baserow_enterprise/api/assistant/views.py +++ b/enterprise/backend/src/baserow_enterprise/api/assistant/views.py @@ -22,9 +22,11 @@ from baserow.api.serializers import get_example_pagination_serializer_class from baserow.api.sessions import set_client_undo_redo_action_group_id from baserow.core.exceptions import UserNotInWorkspace, WorkspaceDoesNotExist -from baserow.core.feature_flags import FF_ASSISTANT, feature_flag_is_enabled from baserow.core.handler import CoreHandler -from baserow_enterprise.assistant.assistant import set_assistant_cancellation_key +from baserow_enterprise.assistant.assistant import ( + check_lm_ready_or_raise, + set_assistant_cancellation_key, +) from baserow_enterprise.assistant.exceptions import ( AssistantChatDoesNotExist, AssistantChatMessagePredictionDoesNotExist, @@ -99,8 +101,6 @@ class AssistantChatsView(APIView): } ) def get(self, request: Request, query_params) -> Response: - feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True) - workspace_id = query_params["workspace_id"] workspace = CoreHandler().get_workspace(workspace_id) @@ -147,8 +147,6 @@ class AssistantChatView(APIView): } ) def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse: - feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True) - ui_context = UIContext.from_validate_request(request, data["ui_context"]) workspace_id = ui_context.workspace.id workspace = CoreHandler().get_workspace(workspace_id) @@ -159,6 +157,7 @@ def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse: context=workspace, ) + check_lm_ready_or_raise() handler = AssistantHandler() chat, _ = handler.get_or_create_chat(request.user, workspace, chat_uuid) @@ -174,7 +173,6 @@ def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse: chat.user.profile.timezone = ui_context.timezone assistant = handler.get_assistant(chat) - assistant.check_llm_ready_or_raise() human_message = HumanMessage(content=data["content"], ui_context=ui_context) async def stream_assistant_messages(): @@ -226,8 +224,6 @@ def _stream_assistant_message(self, message: AssistantMessageUnion) -> str: } ) def get(self, request: Request, chat_uuid: str) -> Response: - feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True) - handler = AssistantHandler() chat = handler.get_chat(request.user, chat_uuid) @@ -265,8 +261,6 @@ def get(self, request: Request, chat_uuid: str) -> Response: } ) def delete(self, request: Request, chat_uuid: str) -> Response: - feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True) - handler = AssistantHandler() chat = handler.get_chat(request.user, chat_uuid) @@ -307,8 +301,6 @@ class AssistantChatMessageFeedbackView(APIView): } ) def put(self, request: Request, message_id: int, data) -> Response: - feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True) - handler = AssistantHandler() message = handler.get_chat_message_by_id(request.user, message_id) try: diff --git a/enterprise/backend/src/baserow_enterprise/apps.py b/enterprise/backend/src/baserow_enterprise/apps.py index 38944c16e2..3541f6b026 100755 --- a/enterprise/backend/src/baserow_enterprise/apps.py +++ b/enterprise/backend/src/baserow_enterprise/apps.py @@ -312,13 +312,7 @@ def ready(self): from baserow_enterprise.assistant.tools import ( CreateBuildersToolType, - CreateFieldsToolType, - CreateTablesToolType, - CreateViewFiltersToolType, - CreateViewsToolType, - CreateWorkflowsToolType, GenerateDatabaseFormulaToolType, - GetRowsToolsToolType, GetTablesSchemaToolType, ListBuildersToolType, ListRowsToolType, @@ -326,7 +320,11 @@ def ready(self): ListViewsToolType, ListWorkflowsToolType, NavigationToolType, + RowsToolFactoryToolType, SearchDocsToolType, + TableAndFieldsToolFactoryToolType, + ViewsToolFactoryToolType, + WorkflowToolFactoryToolType, ) from baserow_enterprise.assistant.tools.registries import ( assistant_tool_registry, @@ -338,18 +336,16 @@ def ready(self): assistant_tool_registry.register(ListBuildersToolType()) assistant_tool_registry.register(CreateBuildersToolType()) assistant_tool_registry.register(ListTablesToolType()) - assistant_tool_registry.register(CreateTablesToolType()) assistant_tool_registry.register(GetTablesSchemaToolType()) - assistant_tool_registry.register(CreateFieldsToolType()) + assistant_tool_registry.register(TableAndFieldsToolFactoryToolType()) assistant_tool_registry.register(GenerateDatabaseFormulaToolType()) assistant_tool_registry.register(ListRowsToolType()) - assistant_tool_registry.register(GetRowsToolsToolType()) + assistant_tool_registry.register(RowsToolFactoryToolType()) assistant_tool_registry.register(ListViewsToolType()) - assistant_tool_registry.register(CreateViewsToolType()) - assistant_tool_registry.register(CreateViewFiltersToolType()) + assistant_tool_registry.register(ViewsToolFactoryToolType()) assistant_tool_registry.register(ListWorkflowsToolType()) - assistant_tool_registry.register(CreateWorkflowsToolType()) + assistant_tool_registry.register(WorkflowToolFactoryToolType()) # The signals must always be imported last because they use the registries # which need to be filled first. @@ -426,7 +422,7 @@ def sync_assistant_knowledge_base(sender, **kwargs): from baserow_enterprise.assistant.tasks import ( sync_assistant_knowledge_base as sync_assistant_knowledge_base_task, ) - from baserow_enterprise.assistant.tools.search_docs.handler import ( + from baserow_enterprise.assistant.tools.search_user_docs.handler import ( KnowledgeBaseHandler, ) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/assistant.py b/enterprise/backend/src/baserow_enterprise/assistant/assistant.py index 15857e53a2..3b3bb99b5c 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/assistant.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/assistant.py @@ -1,10 +1,11 @@ from dataclasses import dataclass from functools import lru_cache -from typing import Any, AsyncGenerator, Callable, TypedDict +from typing import Any, AsyncGenerator, Callable, Tuple, TypedDict from django.conf import settings from django.core.cache import cache from django.utils import translation +from django.utils.translation import gettext as _ import udspy from udspy.callback import BaseCallback @@ -19,7 +20,7 @@ from baserow_enterprise.assistant.tools.registries import assistant_tool_registry from .models import AssistantChat, AssistantChatMessage, AssistantChatPrediction -from .prompts import ASSISTANT_SYSTEM_PROMPT +from .signatures import ChatSignature, RequestRouter from .types import ( AiMessage, AiMessageChunk, @@ -118,47 +119,6 @@ def on_tool_end( self.extend_sources(outputs["sources"]) -class ChatSignature(udspy.Signature): - __doc__ = f"{ASSISTANT_SYSTEM_PROMPT}\n TASK INSTRUCTIONS: \n" - - question: str = udspy.InputField() - context: str = udspy.InputField( - description="Context and facts extracted from the history to help answer the question." - ) - ui_context: dict[str, Any] | None = udspy.InputField( - default=None, - description=( - "The context the user is currently in. " - "It contains information about the user, the workspace, open table, view, etc." - "Whenever make sense, use it to ground your answer." - ), - ) - answer: str = udspy.OutputField() - - -class QuestionContextSummarizationSignature(udspy.Signature): - """ - Extract relevant facts from conversation history that provide context for answering - the current question. Do not answer the question or modify it - only extract and - summarize the relevant historical facts that will help in decision-making. - """ - - question: str = udspy.InputField( - description="The current user question that needs context from history." - ) - previous_messages: list[str] = udspy.InputField( - description="Conversation history as alternating user/assistant messages." - ) - facts: str = udspy.OutputField( - description=( - "Relevant facts extracted from the conversation history as a concise " - "paragraph. Include only information that provides necessary context for " - "answering the question. Do not answer the question itself, do not modify " - "the question, and do not include irrelevant details." - ) - ) - - def get_assistant_cancellation_key(chat_uuid: str) -> str: """ Get the Redis cache key for cancellation tracking. @@ -182,27 +142,69 @@ def set_assistant_cancellation_key(chat_uuid: str, timeout: int = 300) -> None: cache.set(cache_key, True, timeout=timeout) +def get_lm_client( + model: str | None = None, +) -> "Assistant": + """ + Returns a udspy.LM client configured with the specified model or the default model. + + :param model: The language model to use. If None, the default model from settings + will be used. + :return: A udspy.LM instance. + """ + + return udspy.LM(model=model or settings.BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL) + + +@lru_cache(maxsize=1) +def check_lm_ready_or_raise() -> None: + """ + Checks if the configured LLM is ready by making a test call. Raises + AssistantModelNotSupportedError if the model is not supported or accessible. + """ + + lm = get_lm_client() + try: + lm("Respond in JSON: {'response': 'ok'}") + except Exception as e: + raise AssistantModelNotSupportedError( + f"The model '{lm.model}' is not supported or accessible: {e}" + ) + + class Assistant: def __init__(self, chat: AssistantChat): self._chat = chat self._user = chat.user self._workspace = chat.workspace - self._init_lm_client() + self._lm_client = get_lm_client() self._init_assistant() - def _init_lm_client(self): - lm_model = settings.BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL - self._lm_client = udspy.LM(model=lm_model) - def _init_assistant(self): + self.history = None self.tool_helpers = self.get_tool_helpers() - tools = assistant_tool_registry.list_all_usable_tools( - self._user, self._workspace, self.tool_helpers - ) + tools = [ + t if isinstance(t, udspy.Tool) else udspy.Tool(t) + for t in assistant_tool_registry.list_all_usable_tools( + self._user, self._workspace, self.tool_helpers + ) + ] self.callbacks = AssistantCallbacks(self.tool_helpers) - self._assistant = udspy.ReAct(ChatSignature, tools=tools, max_iters=20) - self.history: list[str] = [] + + module_kwargs = { + "temperature": settings.BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE, + "response_format": {"type": "json_object"}, + } + + self.search_user_docs_tool = next( + (tool for tool in tools if tool.name == "search_user_docs"), None + ) + self.agent_tools = tools + self._request_router = udspy.ChainOfThought(RequestRouter, **module_kwargs) + self._assistant = udspy.ReAct( + ChatSignature, tools=self.agent_tools, max_iters=20, **module_kwargs + ) async def acreate_chat_message( self, @@ -277,7 +279,7 @@ def list_chat_messages( ) return list(reversed(messages)) - async def aload_chat_history(self, limit=30): + async def afetch_chat_history(self, limit=30): """ Loads the chat history into a udspy.History object. It only loads complete message pairs (human + AI). The history will be in chronological order and must @@ -287,6 +289,7 @@ async def aload_chat_history(self, limit=30): :return: None """ + history = udspy.History() last_saved_messages: list[AssistantChatMessage] = [ msg async for msg in self._chat.messages.order_by("-created_on")[:limit] ] @@ -301,18 +304,11 @@ async def aload_chat_history(self, limit=30): ): continue - self.history.append(f"Human: {first_message.content}") - ai_answer = last_saved_messages.pop() - self.history.append(f"AI: {ai_answer.content}") + history.add_user_message(first_message.content) + assistant_answer = last_saved_messages.pop() + history.add_assistant_message(assistant_answer.content) - @lru_cache(maxsize=1) - def check_llm_ready_or_raise(self): - try: - self._lm_client("Say ok if you can read this.") - except Exception as e: - raise AssistantModelNotSupportedError( - f"The model '{self._lm_client.model}' is not supported or accessible: {e}" - ) + return history def get_tool_helpers(self) -> ToolHelpers: def update_status_localized(status: str): @@ -330,51 +326,58 @@ def update_status_localized(status: str): navigate_to=unsafe_navigate_to, ) - async def _generate_chat_title( - self, user_message: HumanMessage, ai_msg: AiMessage - ) -> str: + async def _generate_chat_title(self, user_message: str) -> str: """ Generates a title for the chat based on the user message and AI response. + + :param user_message: The latest user message in the chat. + :return: The generated chat title. """ title_generator = udspy.Predict( udspy.Signature.from_string( - "user_message, ai_response -> chat_title", - "Create a short title for the following chat conversation.", + "user_message -> chat_title", + "Create a short title for the following user request.", ) ) rsp = await title_generator.aforward( - user_message=user_message.content, - ai_response=ai_msg.content[:300], + user_message=user_message, ) return rsp.chat_title async def _acreate_ai_message_response( self, human_msg: HumanMessage, - final_prediction: udspy.Prediction, - sources: list[str], + prediction: udspy.Prediction, ) -> AiMessage: + """ + Creates and saves an AI chat message response based on the prediction. Stores + the prediction in AssistantChatPrediction, linking it to the human message, so + it can be referenced later to provide feedback. + + :param human_msg: The human message instance. + :param prediction: The udspy.Prediction instance containing the AI response. + :return: The created AiMessage instance to return to the user. + """ + + sources = self.callbacks.sources ai_msg = await self.acreate_chat_message( AssistantChatMessage.Role.AI, - final_prediction.answer, + prediction.answer, artifacts={"sources": sources}, action_group_id=get_client_undo_redo_action_group_id(self._user), ) + await AssistantChatPrediction.objects.acreate( human_message=human_msg, ai_response=ai_msg, - prediction={ - "model": self._lm_client.model, - "trajectory": final_prediction.trajectory, - "reasoning": final_prediction.reasoning, - }, + prediction={k: v for k, v in prediction.items() if k != "module"}, ) # Yield final complete message return AiMessage( id=ai_msg.id, - content=final_prediction.answer, + content=prediction.answer, sources=sources, can_submit_feedback=True, ) @@ -401,51 +404,48 @@ def _check_cancellation(self, cache_key: str, message_id: str) -> None: cache.delete(cache_key) raise AssistantMessageCancelled(message_id=message_id) - async def _summarize_context_from_history(self, question: str) -> str: + async def get_router_stream( + self, message: HumanMessage + ) -> AsyncGenerator[Any, None]: """ - Extract relevant facts from chat history to provide context for the question or - return an empty string if there is no history. + Returns an async generator that streams the router's response to a user - :param question: The current user question that needs context from history. - :return: A string containing relevant facts from the conversation history. + :param message: The current user message that needs context from history. + :return: An async generator that yields stream events. """ - if not self.history: - return "" + self.history = await self.afetch_chat_history() - predictor = udspy.Predict(QuestionContextSummarizationSignature) - result = await predictor.aforward( - question=question, - previous_messages=self.history, + return self._request_router.astream( + question=message.content, + conversation_history=RequestRouter.format_conversation_history( + self.history + ), ) - return result.facts - async def _process_stream_event( + async def _process_router_stream( self, event: Any, human_msg: AssistantChatMessage, - human_message: HumanMessage, - stream_reasoning: bool, - ) -> tuple[list[AssistantMessageUnion], bool]: + ) -> Tuple[list[AssistantMessageUnion], bool, udspy.Prediction | None]: """ - Process a single event from the output stream. + Process a single event from the smart router output stream. :param event: The event to process. :param human_msg: The human message instance. - :param human_message: The human message data. - :param stream_reasoning: Whether reasoning streaming is enabled. - :return: a tuple of (messages_to_yield, updated_stream_reasoning_flag). + :return: a tuple of (messages_to_yield, prediction). """ messages = [] + prediction = None if isinstance(event, (AiThinkingMessage, AiNavigationMessage)): messages.append(event) - return messages, True # Enable reasoning streaming + return messages, prediction # Stream the final answer if isinstance(event, udspy.OutputStreamChunk): - if event.field_name == "answer": + if event.field_name == "answer" and event.content.strip(): messages.append( AiMessageChunk( content=event.content, @@ -454,28 +454,126 @@ async def _process_stream_event( ) elif isinstance(event, udspy.Prediction): - # sub-module predictions contain reasoning steps - if "next_thought" in event and stream_reasoning: - messages.append(AiReasoningChunk(content=event.next_thought)) + if hasattr(event, "routing_decision"): + prediction = event + + if getattr(event, "routing_decision", None) == "delegate_to_agent": + messages.append(AiThinkingMessage(content=_("Thinking..."))) + elif getattr(event, "routing_decision", None) == "search_user_docs": + if self.search_user_docs_tool is not None: + await self.search_user_docs_tool(question=event.search_query) + else: + messages.append( + AiMessage( + content=_( + "I wanted to search the documentation for you, " + "but the search tool isn't currently available.\n\n" + "To enable documentation search, you'll need to set up " + "the local knowledge base. \n\n" + "You can find setup instructions at: https://baserow.io/user-docs" + ), + sources=[], + ) + ) + elif getattr(event, "answer", None): + ai_msg = await self._acreate_ai_message_response(human_msg, event) + messages.append(ai_msg) - # final prediction contains the answer to the user question - elif event.module is self._assistant: - ai_msg = await self._acreate_ai_message_response( - human_msg, event, self.callbacks.sources + return messages, prediction + + async def _process_agent_stream( + self, + event: Any, + human_msg: AssistantChatMessage, + ) -> Tuple[list[AssistantMessageUnion], udspy.Prediction | None]: + """ + Process a single event from the output stream. + + :param event: The event to process. + :param human_msg: The human message instance. + :return: a tuple of (messages_to_yield, prediction). + """ + + messages = [] + prediction = None + + if isinstance(event, (AiThinkingMessage, AiNavigationMessage)): + messages.append(event) + return messages, prediction + + # Stream the final answer + if isinstance(event, udspy.OutputStreamChunk): + if ( + event.field_name == "answer" + and event.module is self._assistant.extract_module + ): + messages.append( + AiMessageChunk( + content=event.content, + sources=self.callbacks.sources, + ) ) + + elif isinstance(event, udspy.Prediction): + # final prediction contains the answer to the user question + if event.module is self._assistant: + prediction = event + ai_msg = await self._acreate_ai_message_response(human_msg, prediction) messages.append(ai_msg) - # Generate chat title if needed - if not self._chat.title: - chat_title = await self._generate_chat_title(human_message, ai_msg) - messages.append(ChatTitleMessage(content=chat_title)) - self._chat.title = chat_title - await self._chat.asave(update_fields=["title", "updated_on"]) + elif reasoning := getattr(event, "next_thought", None): + messages.append(AiReasoningChunk(content=reasoning)) + + return messages, prediction - return messages, stream_reasoning + def get_agent_stream( + self, message: HumanMessage, extracted_context: str + ) -> AsyncGenerator[Any, None]: + """ + Returns an async generator that streams the ReAct agent's response to a user + message. + + :param user_message: The message from the user. + :return: An async generator that yields stream events. + """ + + ui_context = message.ui_context.format() if message.ui_context else None + + return self._assistant.astream( + question=message.content, + context=extracted_context, + ui_context=ui_context, + ) + + async def _process_stream( + self, + human_msg: HumanMessage, + stream: AsyncGenerator[Any, None], + process_event_func: Callable[ + [Any, AssistantChatMessage], + Tuple[list[AssistantMessageUnion], udspy.Prediction | None], + ], + ) -> AsyncGenerator[Tuple[AssistantMessageUnion, udspy.Prediction | None], None]: + chunk_count = 0 + cancellation_key = self._get_cancellation_cache_key() + message_id = str(human_msg.id) + + async for event in stream: + # Periodically check for cancellation + chunk_count += 1 + if chunk_count % 10 == 0: + self._check_cancellation(cancellation_key, message_id) + + messages, prediction = await process_event_func(event, human_msg) + + if messages: # Don't return responses if cancelled + self._check_cancellation(cancellation_key, message_id) + + for msg in messages: + yield msg, prediction async def astream_messages( - self, human_message: HumanMessage + self, message: HumanMessage ) -> AsyncGenerator[AssistantMessageUnion, None]: """ Streams the response to a user message. @@ -483,47 +581,44 @@ async def astream_messages( :param human_message: The message from the user. :return: An async generator that yields the response messages. """ + + human_msg = await self.acreate_chat_message( + AssistantChatMessage.Role.HUMAN, + message.content, + ) + with udspy.settings.context( lm=self._lm_client, callbacks=[*udspy.settings.callbacks, self.callbacks], ): - if self.history is None: - await self.aload_chat_history() - - context_from_history = await self._summarize_context_from_history( - human_message.content - ) - - output_stream = self._assistant.astream( - question=human_message.content, - context=context_from_history, - ui_context=human_message.ui_context.model_dump_json(exclude_none=True), - ) - - human_msg = await self.acreate_chat_message( - AssistantChatMessage.Role.HUMAN, human_message.content - ) - - cache_key = self._get_cancellation_cache_key() message_id = str(human_msg.id) yield AiStartedMessage(message_id=message_id) - # Flag to wait for the first step in the reasoning to start streaming it - stream_reasoning = False - chunk_count = 0 + router_stream = await self.get_router_stream(message) + routing_decision, extracted_context = None, "" - async for event in output_stream: - # Periodically check for cancellation - chunk_count += 1 - if chunk_count % 10 == 0: - self._check_cancellation(cache_key, message_id) - - messages, stream_reasoning = await self._process_stream_event( - event, human_msg, human_message, stream_reasoning + async for msg, prediction in self._process_stream( + human_msg, router_stream, self._process_router_stream + ): + if prediction is not None: + routing_decision = prediction.routing_decision + extracted_context = prediction.extracted_context + yield msg + + if routing_decision == "delegate_to_agent": + agent_stream = self.get_agent_stream( + message, + extracted_context=extracted_context, ) - if messages: # Don't return responses if cancelled - self._check_cancellation(cache_key, message_id) + async for msg, __ in self._process_stream( + human_msg, agent_stream, self._process_agent_stream + ): + yield msg - for msg in messages: - yield msg + # Generate chat title if needed + if not self._chat.title: + chat_title = await self._generate_chat_title(human_msg.content) + self._chat.title = chat_title + await self._chat.asave(update_fields=["title", "updated_on"]) + yield ChatTitleMessage(content=chat_title) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/prompts.py b/enterprise/backend/src/baserow_enterprise/assistant/prompts.py index 0c4f0eaa54..7e41db1843 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/prompts.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/prompts.py @@ -57,29 +57,47 @@ • **Publishing**: Requires at least one configured action """ -ASSISTANT_SYSTEM_PROMPT = ( +AGENT_LIMITATIONS = """ +## LIMITATIONS + +### CANNOT CREATE: +• User accounts, workspaces +• Applications, pages +• Dashboards, widgets +• Snapshots, webhooks, integrations +• Roles, permissions + +### CANNOT UPDATE/MODIFY: +• User, workspace, or integration settings +• Roles, permissions +• Applications, pages +• Dashboards, widgets + +### CANNOT DELETE: +• Users, workspaces +• Roles, permissions +• Applications, pages +• Dashboards, widgets +""" + +ASSISTANT_SYSTEM_PROMPT_BASE = ( f""" You are Kuma, an AI expert for Baserow (open-source no-code platform). ## YOUR KNOWLEDGE 1. **Core concepts** (below) -2. **Detailed docs** - use search_docs tool to search when needed +2. **Detailed docs** - use search_user_docs tool to search when needed 3. **API specs** - guide users to "{settings.PUBLIC_BACKEND_URL}/api/schema.json" 4. **Official website** - "https://baserow.io" +5. **Community support** - "https://community.baserow.io" +6. **Direct support** - for Advanced/Enterprise plan users -## HOW TO HELP +## ANSWER FORMATTING GUIDELINES • Use American English spelling and grammar -• Be clear, concise, and actionable -• For troubleshooting: ask for error messages or describe expected vs actual results -• **NEVER** fabricate answers or URLs. Acknowledge when you can't be sure. -• Use the tools whenever possible. Fallback to search_docs and provide instruction only when it's not possible to fulfill the request. Ground answers in the documentation. -• When finished, briefly suggest one or more logical next steps only if they use tools you have access to and directly builds on what was just done. - -## FORMATTING (CRITICAL) • Only use Markdown (bold, italics, lists, code blocks) -• Prefer lists in explanations. Numbered lists for steps; bulleted for others +• Prefer lists in explanations. Numbered lists for steps; bulleted for others. • Use code blocks for examples, commands, snippets -• EXCEPTION: When showing database schema or query results, tables are acceptable +• Be concise and clear in your response ## BASEROW CONCEPTS """ @@ -88,3 +106,107 @@ + APPLICATION_BUILDER_CONCEPTS + AUTOMATION_BUILDER_CONCEPTS ) + +AGENT_SYSTEM_PROMPT = ( + ASSISTANT_SYSTEM_PROMPT_BASE + + """ +**CRITICAL:** You MUST use your action tools to fulfill the request, loading additional tools if needed. + +### YOUR TOOLS: +- **Action tools**: Navigate, list databases, tables, fields, views, filters, workflows, rows, etc. +- **Tool loaders**: Load additional specialized tools (e.g., load_rows_tools, load_views_tools). Use them to access capabilities not currently available. + +**IMPORTANT - HOW TO UNDERSTAND YOUR TOOLS:** +- Read each tool's NAME, DESCRIPTION, and ARGUMENTS carefully +- Tool names and descriptions tell you what they do (e.g., "list_tables", "create_rows_in_table_X") +- Arguments show what inputs they need +- **NEVER use search_user_docs to learn about tools** - it contains end-user documentation, NOT information about which tools to use or how to call them +- Inspect available tools directly to decide what to use + +### HOW TO WORK: +1. **Use action tools** to accomplish the user's goal +2. **If a needed tool isn't available**, call a tool loader to load it (e.g., if you need to create a field but don't have the tool, load field creation tools) +3. **Keep using tools** until the goal is reached or you confirm NO tool can help and NO tool loader can provide the needed tool + +### EXAMPLE - CORRECT USE OF TOOL LOADERS: +**User request:** "Change all 'Done' tasks to 'Todo'" + +**CORRECT approach:** +✓ Step 1: Identify that Tasks is a table in the open database, and status is the field to update +✓ Step 2: Notice you need to update rows but don't have the tool +✓ Step 3: Call the row tool loader (e.g., `load_rows_tools` for table X, requesting update capabilities) +✓ Step 4: Use the newly loaded `update_rows` tool to update the rows +✓ Step 5: Complete the task + +**CRITICAL:** Before giving up, ALWAYS check if a tool loader can provide the necessary tools to complete the task. + +### IF YOU CANNOT COMPLETE THE REQUEST: +If you've exhausted all available tools and loaders and cannot complete the task, offer: "I wasn't able to complete this using my available tools. Would you like me to search the documentation for instructions on how to do this manually?" + +### YOUR PRIORITY: +1. **First**: Use action tools to complete the request +2. **If tool missing**: Try loading it with a tool loader (scan all available loaders) +3. **If truly unable**: Explain the issue and offer to search documentation (never provide instructions from memory) + +The router determined this requires action. You were chosen because the user wants you to DO something, not provide information. + +Be aware of your limitations. If users ask for something outside your capabilities, finish immediately, explain what you can and cannot do based on the limitations below, and offer to search the documentation for further help. +""" + + AGENT_LIMITATIONS + + """ +### TASK INSTRUCTIONS: +""" +) + + +REQUEST_ROUTER_PROMPT = ( + ASSISTANT_SYSTEM_PROMPT_BASE + + """ +Route based on what the user wants YOU to do: + +**delegate_to_agent** (DEFAULT) - User wants YOU to perform an action +- Commands/requests for YOU: "Create...", "Delete...", "Update...", "Add...", "Show me...", "List...", "Find..." +- Vague/unclear requests +- Anything not explicitly asking for instructions + +**search_user_docs** - User wants to learn HOW TO do something themselves +- ONLY when explicitly asking for instructions: "How do I...", "How can I...", "What are the steps to..." +- ONLY when asking for explanations: "What is...", "What does... mean", "Explain..." +- NOT for action requests even if phrased as questions + +## Critical Rules +- "Create X" → delegate_to_agent (action request for YOU) +- "How do I create X?" → search_user_docs (asking for instructions) +- When uncertain → delegate_to_agent + +## Output Requirements +**delegate_to_agent:** +- extracted_context: Comprehensive details from conversation history (IDs, names, actions, specs) +- search_query: empty + +**search_user_docs:** +- search_query: Clear question using Baserow terminology and the answer language if not English +- extracted_context: empty + +## Examples + +**Example 1 - delegate_to_agent (action):** +question: "Create a calendar view" +→ routing_decision: "delegate_to_agent" +→ search_query: "" +→ extracted_context: "User wants to create a calendar view." + +**Example 2 - search_user_docs (instructions):** +question: "How do I create a calendar view?" +→ routing_decision: "search_user_docs" +→ search_query: "How to create a calendar view in Baserow" +→ extracted_context: "" + +**Example 3 - delegate_to_agent (with history):** +question: "Assign them to Bob" +conversation_history: ["[0] (user): Show urgent tasks", "[1] (assistant): Found 5 tasks in table 'Tasks' (ID: 123)"] +→ routing_decision: "delegate_to_agent" +→ search_query: "" +→ extracted_context: "User wants to assign urgent tasks to Bob. Tasks in table 'Tasks' (ID: 123). Found 5 urgent tasks." +""" +) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/signatures.py b/enterprise/backend/src/baserow_enterprise/assistant/signatures.py new file mode 100644 index 0000000000..89bf3d0dd1 --- /dev/null +++ b/enterprise/backend/src/baserow_enterprise/assistant/signatures.py @@ -0,0 +1,67 @@ +from typing import Literal + +import udspy + +from .prompts import AGENT_SYSTEM_PROMPT, REQUEST_ROUTER_PROMPT + + +class ChatSignature(udspy.Signature): + __doc__ = AGENT_SYSTEM_PROMPT + + question: str = udspy.InputField() + context: str = udspy.InputField( + description="Context and facts extracted from the history to help answer the question." + ) + ui_context: str | None = udspy.InputField( + default=None, + description=( + "The JSON serialized context the user is currently in. " + "It contains information about the user, the timezone, the workspace, etc." + "Whenever make sense, use it to ground your answer." + ), + ) + answer: str = udspy.OutputField() + + +class RequestRouter(udspy.Signature): + __doc__ = REQUEST_ROUTER_PROMPT + + question: str = udspy.InputField(desc="The current user question to route") + conversation_history: list[str] = udspy.InputField( + desc="Previous messages formatted as '[index] (role): content', ordered chronologically" + ) + + routing_decision: Literal[ + "delegate_to_agent", "search_user_docs" + ] = udspy.OutputField( + desc="Must be one of: 'delegate_to_agent' or 'search_user_docs'" + ) + extracted_context: str = udspy.OutputField( + desc=( + "Relevant context extracted from conversation history. " + "The agent won't see the full history, only the question and this extracted context. " + "Always fill with comprehensive details (IDs, names, actions, specifications). " + "Be verbose - include all relevant information to help understand the request." + ), + ) + search_query: str = udspy.OutputField( + desc=( + "The search query in English to use with search_user_docs if routing_decision='search_user_docs'. " + "Should be a clear, well-formulated question using Baserow terminology. " + "Empty string if routing_decision='delegate_to_agent'. " + "If the question is in another language, make sure to mention in which " + "language the answer should be." + ) + ) + + @classmethod + def format_conversation_history(cls, history: udspy.History) -> list[str]: + """ + Format the conversation history into a list of strings for the signature. + """ + + formatted_history = [] + for i, msg in enumerate(history.messages): + formatted_history.append(f"[{i}] ({msg['role']}): {msg['content']}") + + return formatted_history diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/__init__.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/__init__.py index ac6006dbb9..4ac95ed235 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/__init__.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/__init__.py @@ -2,4 +2,4 @@ from .core.tools import * # noqa: F401, F403 from .database.tools import * # noqa: F401, F403 from .navigation.tools import * # noqa: F401, F403 -from .search_docs.tools import * # noqa: F401, F403 +from .search_user_docs.tools import * # noqa: F401, F403 diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/__init__.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/__init__.py index ca44da1dd7..ace1c221c3 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/__init__.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/__init__.py @@ -1,6 +1,6 @@ -from .tools import CreateWorkflowsToolType, ListWorkflowsToolType +from .tools import ListWorkflowsToolType, WorkflowToolFactoryToolType __all__ = [ "ListWorkflowsToolType", - "CreateWorkflowsToolType", + "WorkflowToolFactoryToolType", ] diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/tools.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/tools.py index 11cc03b38e..6046ca5ffa 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/tools.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/automation/tools.py @@ -4,6 +4,8 @@ from django.db import transaction from django.utils.translation import gettext as _ +import udspy + from baserow.contrib.automation.workflows.service import AutomationWorkflowService from baserow.core.models import Workspace from baserow_enterprise.assistant.tools.registries import AssistantToolType @@ -47,13 +49,9 @@ def list_workflows(automation_id: int) -> dict[str, Any]: return list_workflows -def get_create_workflows_tool( +def get_workflow_tool_factory( user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" ) -> Callable[[int, list[WorkflowCreate]], dict[str, list[dict]]]: - """ - Create new workflows. - """ - def create_workflows( automation_id: int, workflows: list[WorkflowCreate] ) -> dict[str, Any]: @@ -100,7 +98,35 @@ def create_workflows( return {"created_workflows": created} - return create_workflows + def load_workflow_automation_tools(): + """ + TOOL LOADER: Loads tools to manage workflows in an automation. + + After calling this loader, you will have access to: + - create_workflows: Create workflows with triggers, actions, and routers + + Use this when you need to create workflows in an automation but don't have the tool. + """ # noqa: W505 + + @udspy.module_callback + def _load_workflow_automation_tools(context): + nonlocal user, workspace, tool_helpers + + observation = ["New tools are now available.\n"] + + create_tool = udspy.Tool(create_workflows) + new_tools = [create_tool] + observation.append( + "- Use `create_workflows` to create workflows in an automation." + ) + + # Re-initialize the module with the new tools for the next iteration + context.module.init_module(tools=context.module._tools + new_tools) + return "\n".join(observation) + + return _load_workflow_automation_tools + + return load_workflow_automation_tools # ============================================================================ @@ -116,9 +142,9 @@ def get_tool(cls, user, workspace, tool_helpers): return get_list_workflows_tool(user, workspace, tool_helpers) -class CreateWorkflowsToolType(AssistantToolType): - type = "create_workflows" +class WorkflowToolFactoryToolType(AssistantToolType): + type = "workflow_tool_factory" @classmethod def get_tool(cls, user, workspace, tool_helpers): - return get_create_workflows_tool(user, workspace, tool_helpers) + return get_workflow_tool_factory(user, workspace, tool_helpers) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/tools.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/tools.py index 3d3cfc53ec..54e8b03994 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/tools.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/tools.py @@ -171,13 +171,32 @@ def get_tool( return get_tables_schema_tool(user, workspace, tool_helpers) -def get_create_tables_tool( +def get_table_and_fields_tools_factory( user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" ) -> Callable[[list[TableItemCreate]], list[dict[str, Any]]]: - """ - Returns a function that creates a set of tables in a given database the user has - access to - """ + def create_fields( + table_id: int, fields: list[AnyFieldItemCreate] + ) -> list[AnyFieldItem]: + """ + Creates fields in the specified table. + + - Choose the most appropriate field type for each field. + - Field names must be unique within a table: check existing names + when needed and skip duplicates. + - For link_row fields, ensure the linked table already exists in + the same database; create it first if needed. + """ + + nonlocal user, workspace, tool_helpers + + if not fields: + return [] + + table = utils.filter_tables(user, workspace).get(id=table_id) + + with transaction.atomic(): + created_fields = utils.create_fields(user, table, fields, tool_helpers) + return {"created_fields": [field.model_dump() for field in created_fields]} def create_tables( database_id: int, tables: list[TableItemCreate], add_sample_rows: bool = True @@ -276,64 +295,48 @@ def create_tables( "notes": notes, } - return create_tables - - -class CreateTablesToolType(AssistantToolType): - type = "create_tables" - - @classmethod - def get_tool( - cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" - ) -> Callable[[Any], Any]: - return get_create_tables_tool(user, workspace, tool_helpers) - + def load_table_and_fields_tools(): + """ + TOOL LOADER: Loads table and field creation tools for a database. -def get_create_fields_tool( - user: AbstractUser, - workspace: Workspace, - tool_helpers: "ToolHelpers", -) -> Callable[[int, list[AnyFieldItemCreate]], list[dict[str, Any]]]: - """ - Returns a function that creates fields in a given table the user has access to - in the current workspace. - """ + After calling this loader, you will have access to: + - create_tables: Create new tables in a database with fields and sample rows + - create_fields: Add new fields to an existing table - def create_fields( - table_id: int, fields: list[AnyFieldItemCreate] - ) -> list[AnyFieldItem]: + Use this when you need to create tables or add fields but don't have the tools. """ - Creates fields in the specified table. - - Choose the most appropriate field type for each field. - - Field names must be unique within a table: check existing names - when needed and skip duplicates. - - For link_row fields, ensure the linked table already exists in - the same database; create it first if needed. - """ + @udspy.module_callback + def _load_table_and_fields_tools(context): + nonlocal user, workspace, tool_helpers - nonlocal user, workspace, tool_helpers + observation = ["New tools are now available.\n"] - if not fields: - return [] + create_tool = udspy.Tool(create_tables) + new_tools = [create_tool] + observation.append("- Use `create_tables` to create tables in a database.") - table = utils.filter_tables(user, workspace).get(id=table_id) + create_fields_tool = udspy.Tool(create_fields) + new_tools.append(create_fields_tool) + observation.append("- Use `create_fields` to create fields in a table.") - with transaction.atomic(): - created_fields = utils.create_fields(user, table, fields, tool_helpers) - return {"created_fields": [field.model_dump() for field in created_fields]} + # Re-initialize the module with the new tools for the next iteration + context.module.init_module(tools=context.module._tools + new_tools) + return "\n".join(observation) + + return _load_table_and_fields_tools - return create_fields + return load_table_and_fields_tools -class CreateFieldsToolType(AssistantToolType): - type = "create_fields" +class TableAndFieldsToolFactoryToolType(AssistantToolType): + type = "table_and_fields_tool_factory" @classmethod def get_tool( cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" ) -> Callable[[Any], Any]: - return get_create_fields_tool(user, workspace, tool_helpers) + return get_table_and_fields_tools_factory(user, workspace, tool_helpers) def get_list_rows_tool( @@ -347,7 +350,7 @@ def get_list_rows_tool( def list_rows( table_id: int, offset: int = 0, - limit: int = 10, + limit: int = 20, field_ids: list[int] | None = None, ) -> list[dict[str, Any]]: """ @@ -395,34 +398,43 @@ def get_tool( return get_list_rows_tool(user, workspace, tool_helpers) -def get_rows_meta_tool( +def get_rows_tools_factory( user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers", ) -> Callable[[int, list[dict[str, Any]]], list[Any]]: - """ - Returns a meta-tool that, given a table ID, returns an observation that says that - new tools are available and a list of tools to create, update and delete rows - in that table. - """ - - def get_rows_tools( + def load_rows_tools( table_ids: list[int], operations: list[Literal["create", "update", "delete"]], ) -> Tuple[str, list[Callable[[Any], Any]]]: """ - Generates row operation tools for specified tables. Required before: - create/update/delete rows. + TOOL LOADER: Loads row manipulation tools for specified tables. + Make sure to have the correct table IDs. + + After calling this loader, you will have access to table-specific tools: + - create_rows_in_table_X: Create new rows in table X + - update_rows_in_table_X: Update existing rows in table X by their IDs + - delete_rows_in_table_X: Delete rows from table X by their IDs + + Use this when you need to create, update, or delete rows but don't have + the tools. + Call with the table IDs and desired operations (create/update/delete). """ @udspy.module_callback - def load_rows_tools(context): + def _load_rows_tools(context): nonlocal user, workspace, tool_helpers - observation = ["New tools are now available.\n"] + tables = utils.filter_tables(user, workspace).filter(id__in=table_ids) + if not tables: + observation = [ + "No valid tables found for the given IDs. ", + "Make sure the table IDs are correct.", + ] + return "\n".join(observation) new_tools = [] - tables = utils.filter_tables(user, workspace).filter(id__in=table_ids) + observation = ["New tools are now available.\n"] for table in tables: table_tools = utils.get_table_rows_tools( user, workspace, tool_helpers, table @@ -455,19 +467,19 @@ def load_rows_tools(context): context.module.init_module(tools=context.module._tools + new_tools) return "\n".join(observation) - return load_rows_tools + return _load_rows_tools - return get_rows_tools + return load_rows_tools -class GetRowsToolsToolType(AssistantToolType): - type = "get_rows_tools" +class RowsToolFactoryToolType(AssistantToolType): + type = "rows_tool_factory" @classmethod def get_tool( cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" ) -> Callable[[Any], Any]: - return get_rows_meta_tool(user, workspace, tool_helpers) + return get_rows_tools_factory(user, workspace, tool_helpers) def get_list_views_tool( @@ -523,13 +535,46 @@ def get_tool( return get_list_views_tool(user, workspace, tool_helpers) -def get_create_views_tool( +def get_views_tool_factory( user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" ) -> Callable[[int, list[str]], list[str]]: - """ - Returns a function that creates views in a given table the user has access to - in the current workspace. - """ + def create_view_filters( + view_filters: list[ViewFiltersArgs], + ) -> list[AnyViewFilterItem]: + """ + Creates filters in the specified views. + """ + + nonlocal user, workspace, tool_helpers + + if not view_filters: + return [] + + created_view_filters = [] + for vf in view_filters: + orm_view = utils.get_view(user, vf.view_id) + tool_helpers.update_status( + _("Creating filters in %(view_name)s...") % {"view_name": orm_view.name} + ) + + fields = {f.id: f for f in orm_view.table.field_set.all()} + created_filters = [] + with transaction.atomic(): + for filter in vf.filters: + try: + orm_filter = utils.create_view_filter( + user, orm_view, fields, filter + ) + except ValueError as e: + logger.warning(f"Skipping filter creation: {e}") + continue + + created_filters.append({"id": orm_filter.id, **filter.model_dump()}) + created_view_filters.append( + {"view_id": vf.view_id, "filters": created_filters} + ) + + return {"created_view_filters": created_view_filters} def create_views( table_id: int, views: list[AnyViewItemCreate] @@ -540,7 +585,7 @@ def create_views( - Choose the most appropriate view type for each view. - View names must be unique within a table: check existing names when needed and - skip duplicates. + avoid duplicates. """ nonlocal user, workspace, tool_helpers @@ -584,80 +629,51 @@ def create_views( return {"created_views": created_views} - return create_views - - -class CreateViewsToolType(AssistantToolType): - type = "create_views" - - @classmethod - def get_tool( - cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" - ) -> Callable[[Any], Any]: - return get_create_views_tool(user, workspace, tool_helpers) + def load_views_tools(): + """ + TOOL LOADER: Loads tools to manage views and filters + (grid, gallery, form, kanban, calendar and timeline). + After calling this loader, you will be able to: + - create_views: Create grid, gallery, form, kanban, calendar and timeline views + - create_view_filters: Create filters for specific views to filter rows -def get_create_view_filters_tool( - user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" -) -> Callable[[int, list[str]], list[str]]: - """ - Returns a function that creates views in a given table the user has access to - in the current workspace. - """ - - def create_view_filters( - view_filters: list[ViewFiltersArgs], - ) -> list[AnyViewFilterItem]: + Use this when you need to create views or filters but don't have the tools yet. """ - Creates filters in the specified view. - - Choose the most appropriate filter for each view. - - Filter names must be unique within a view: check existing names - when needed and skip duplicates. - """ + @udspy.module_callback + def _load_views_tools(context): + nonlocal user, workspace, tool_helpers - nonlocal user, workspace, tool_helpers + observation = ["New tools are now available.\n"] - if not view_filters: - return [] + create_tool = udspy.Tool(create_views) + new_tools = [create_tool] + observation.append("- Use `create_views` to create views.") - created_view_filters = [] - for vf in view_filters: - orm_view = utils.get_view(user, vf.view_id) - tool_helpers.update_status( - _("Creating filters in %(view_name)s...") % {"view_name": orm_view.name} + create_filters_tool = udspy.Tool(create_view_filters) + new_tools.append(create_filters_tool) + observation.append( + "- Use `create_view_filters` to create filters in views." ) - fields = {f.id: f for f in orm_view.table.field_set.all()} - created_filters = [] - with transaction.atomic(): - for filter in vf.filters: - try: - orm_filter = utils.create_view_filter( - user, orm_view, fields, filter - ) - except ValueError as e: - logger.warning(f"Skipping filter creation: {e}") - continue - - created_filters.append({"id": orm_filter.id, **filter.model_dump()}) - created_view_filters.append( - {"view_id": vf.view_id, "filters": created_filters} - ) + # Re-initialize the module with the new tools for the next iteration + context.module.init_module(tools=context.module._tools + new_tools) + return "\n".join(observation) - return {"created_view_filters": created_view_filters} + return _load_views_tools - return create_view_filters + return load_views_tools -class CreateViewFiltersToolType(AssistantToolType): - type = "create_view_filters" +class ViewsToolFactoryToolType(AssistantToolType): + type = "views_tool_factory" @classmethod def get_tool( cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" ) -> Callable[[Any], Any]: - return get_create_view_filters_tool(user, workspace, tool_helpers) + return get_views_tool_factory(user, workspace, tool_helpers) def get_formula_type_tool( diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/table.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/table.py index ae49ebc265..3703e877bc 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/table.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/table.py @@ -42,17 +42,20 @@ class TableItem(BaseTableItem): class ListTablesFilterArg(BaseModel): database_ids: list[int] | None = Field( - ..., description="A list of database_ids to filter. None to exclude this filter" + default=None, + description="A list of database_ids to filter. None to exclude this filter", ) database_names: list[str] | None = Field( - ..., + default=None, description="A list of database_names to filter. None to exclude this filter", ) table_ids: list[int] | None = Field( - ..., description="A list of table ids to filter. None to exclude this filter" + default=None, + description="A list of table ids to filter. None to exclude this filter", ) table_names: list[str] | None = Field( - ..., description="A list of table names to filter. None to exclude this filter" + default=None, + description="A list of table names to filter. None to exclude this filter", ) def to_orm_filter(self) -> Q: diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/views.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/views.py index 3395bd782a..e28a7438f4 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/views.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/types/views.py @@ -21,7 +21,7 @@ class ViewItemCreate(BaseModel): description="A sensible name for the view (i.e. 'Pending payments', 'Completed tasks', etc.).", ) public: bool = Field( - ..., + default=False, description="Whether the view is publicly accessible. False unless specified.", ) @@ -50,11 +50,11 @@ def from_django_orm(cls, orm_view: Type[BaserowView]) -> "ViewItem": class GridFieldOption(BaseModel): field_id: int = Field(...) width: int = Field( - ..., + default=200, description="The width of the field in the grid view. Default is 200.", ) hidden: bool = Field( - ..., + default=False, description="Whether the field is hidden in the grid view. Default is False.", ) @@ -152,7 +152,7 @@ def from_django_orm(cls, orm_view: CalendarView) -> "CalendarViewItem": class BaseGalleryViewItem(ViewItemCreate): type: Literal["gallery"] = Field(..., description="A gallery view.") cover_field_id: int | None = Field( - None, + default=None, description=( "The ID of the field to use for the gallery cover image. Must be a file field. None if no file field is available." ), @@ -239,43 +239,42 @@ class FormFieldOption(BaseModel): field_id: int = Field(..., description="The ID of the field.") name: str = Field(..., description="The name to show for the field in the form.") description: str = Field( - ..., description="The description to show for the field in the form." + default="", description="The description to show for the field in the form." ) required: bool = Field( - ..., description="Whether the field is required in the form. Default is True." + default=True, + description="Whether the field is required in the form. Default is True.", ) order: int = Field(..., description="The order of the field in the form.") class BaseFormViewItem(ViewItemCreate): type: Literal["form"] = Field(..., description="A form view.") - title: str = Field(..., description="The title of the form. Can be empty.") - description: str = Field( - ..., description="The description of the form. Can be empty." - ) + title: str = Field(..., description="The title of the form.") + description: str = Field(..., description="The description of the form.") submit_button_label: str = Field( - ..., description="The label of the submit button. Default is 'Submit'." + default="Submit", description="The label of the submit button." ) receive_notification_on_submit: bool = Field( - ..., + default=False, description=( - "Whether to receive an email notification when the form is submitted. Default is False." + "Whether to receive an email notification when the form is submitted." ), ) submit_action: Literal["MESSAGE", "REDIRECT"] = Field( - ..., - description="The action to perform when the form is submitted. Default is 'MESSAGE'.", + default="MESSAGE", + description="The action to perform when the form is submitted.", ) submit_action_message: str = Field( - ..., + default="", description=( - "The message to display when the form is submitted and the action is 'MESSAGE'. Default is empty." + "The message to display when the form is submitted and the action is 'MESSAGE'." ), ) submit_action_redirect_url: str = Field( - ..., + default="", description=( - "The URL to redirect to when the form is submitted and the action is 'REDIRECT'. Default is empty." + "The URL to redirect to when the form is submitted and the action is 'REDIRECT'." ), ) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/utils.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/utils.py index c7d13c8a06..8cb96df372 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/database/utils.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/database/utils.py @@ -472,7 +472,7 @@ def _update_rows( ) update_rows_tool = udspy.Tool( func=_update_rows, - name=f"update_rows_in_table_{table.id}_by_row_ids", + name=f"update_rows_in_table_{table.id}", description=f"Updates existing rows in the table {table.name} (ID: {table.id}), identified by their row IDs. Max 20 at a time.", args={ "rows": { @@ -504,7 +504,7 @@ def _delete_rows(row_ids: list[int]) -> str: delete_rows_tool = udspy.Tool( func=_delete_rows, - name=f"delete_rows_in_table_{table.id}_by_row_ids", + name=f"delete_rows_in_table_{table.id}", description=f"Deletes rows in the table {table.name} (ID: {table.id}). Max 20 at a time.", args={ "row_ids": { diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/tools.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/tools.py deleted file mode 100644 index 619902489e..0000000000 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/tools.py +++ /dev/null @@ -1,103 +0,0 @@ -from typing import TYPE_CHECKING, Annotated, Any, Callable - -from django.contrib.auth.models import AbstractUser -from django.utils.translation import gettext as _ - -import udspy - -from baserow.core.models import Workspace -from baserow_enterprise.assistant.tools.registries import AssistantToolType - -from .handler import KnowledgeBaseHandler - -if TYPE_CHECKING: - from baserow_enterprise.assistant.assistant import ToolHelpers - -MAX_SOURCES = 3 - - -class SearchDocsSignature(udspy.Signature): - """ - Search the Baserow documentation for relevant information to answer user questions. - Never fabricate answers or URLs. Always copy instructions exactly as they appear in - the documentation, without rephrasing. - """ - - question: str = udspy.InputField() - context: list[str] = udspy.InputField() - response: str = udspy.OutputField() - sources: list[str] = udspy.OutputField( - desc=f"List of unique and relevant source URLs. Max {MAX_SOURCES}." - ) - reliability: float = udspy.OutputField( - desc=( - "The reliability score of the response, from 0 to 1. " - "1 means the answer is fully supported by the provided context. " - "0 means the answer is not supported by the provided context." - ) - ) - - -class SearchDocsRAG(udspy.Module): - def __init__(self): - self.rag = udspy.ChainOfThought(SearchDocsSignature) - - def forward(self, question: str, *args, **kwargs): - context = KnowledgeBaseHandler().search(question, num_results=7) - return self.rag(context=context, question=question) - - -def get_search_docs_tool( - user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" -) -> Callable[[str], dict[str, Any]]: - """ - Returns a function that searches the Baserow documentation for a given query. - """ - - def search_docs( - question: Annotated[ - str, "The English version of the user question, using Baserow vocabulary." - ] - ) -> dict[str, Any]: - """ - Search Baserow documentation for relevant information. Make sure the question - is in English and uses Baserow-specific terminology to get the best results. - """ - - nonlocal tool_helpers - - tool_helpers.update_status(_("Exploring the knowledge base...")) - - search_tool = SearchDocsRAG() - answer = search_tool(question=question) - # Somehow sources can be objects with an "url" attribute instead of strings, - # let's fix that - fixed_sources = [] - for src in answer.sources[:MAX_SOURCES]: - if isinstance(src, str): - fixed_sources.append(src) - elif isinstance(src, dict) and "url" in src: - fixed_sources.append(src["url"]) - - return { - "response": answer.response, - "sources": fixed_sources, - "reliability": answer.reliability, - } - - return search_docs - - -class SearchDocsToolType(AssistantToolType): - type = "search_docs" - - def can_use( - self, user: AbstractUser, workspace: Workspace, *args, **kwargs - ) -> bool: - return KnowledgeBaseHandler().can_search() - - @classmethod - def get_tool( - cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" - ) -> Callable[[Any], Any]: - return get_search_docs_tool(user, workspace, tool_helpers) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/__init__.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/__init__.py similarity index 100% rename from enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/__init__.py rename to enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/__init__.py diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/handler.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/handler.py similarity index 97% rename from enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/handler.py rename to enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/handler.py index 2596548d2e..b554c57af6 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/handler.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/handler.py @@ -102,21 +102,18 @@ def embed_knowledge_chunks( return self.embed_texts([chunk.content for chunk in chunks]) - def query(self, query: str, num_results: int = 10) -> list[str]: + def query(self, query: str, num_results: int = 10) -> list[KnowledgeBaseChunk]: """ Retrieve the most relevant document chunks for the given query. It vectorizes the query and performs a similarity search using the vector field. :param query: The text query to search for :param num_results: The number of results to return - :return: A list of document chunk contents matching the query + :return: A list of KnowledgeBaseChunk instances matching the query """ (vector_query,) = self.embed_texts([query]) - results = self.raw_query(vector_query, num_results=num_results) - response = [res.content for res in results] - - return response + return self.raw_query(vector_query, num_results=num_results) def raw_query( self, query_vector: list[float], num_results: int = 10 @@ -133,6 +130,7 @@ def raw_query( KnowledgeBaseChunk.objects.filter( source_document__status=KnowledgeBaseDocument.Status.READY, ) + .select_related("source_document") .alias( distance=L2Distance(KnowledgeBaseChunk.VECTOR_FIELD_NAME, query_vector) ) @@ -185,13 +183,13 @@ def can_search(self) -> bool: ).exists() ) - def search(self, query: str, num_results=10) -> list[str]: + def search(self, query: str, num_results=10) -> list[KnowledgeBaseChunk]: """ Retrieve the most relevant knowledge chunks for the given query. :param query: The text query to search for :param num_results: The number of results to return - :return: A list of document chunk contents matching the query + :return: A list of KnowledgeBaseChunk instances matching the query """ return self.vector_handler.query(query, num_results=num_results) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/tools.py b/enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/tools.py new file mode 100644 index 0000000000..b7b7480ccd --- /dev/null +++ b/enterprise/backend/src/baserow_enterprise/assistant/tools/search_user_docs/tools.py @@ -0,0 +1,156 @@ +from typing import TYPE_CHECKING, Annotated, Any, Callable + +from django.contrib.auth.models import AbstractUser +from django.utils.translation import gettext as _ + +import udspy +from asgiref.sync import sync_to_async + +from baserow.core.models import Workspace +from baserow_enterprise.assistant.models import KnowledgeBaseChunk +from baserow_enterprise.assistant.tools.registries import AssistantToolType + +from .handler import KnowledgeBaseHandler + +if TYPE_CHECKING: + from baserow_enterprise.assistant.assistant import ToolHelpers + + +class SearchDocsSignature(udspy.Signature): + """ + Given a user question and the relevant documentation chunks as context, provide a an + accurate and concise answer along with a reliability score. If the documentation + provides instructions or URLs, include them in the answer. If the answer is not + found in the context, respond with "Nothing found in the documentation." + + Never fabricate answers or URLs. + """ + + question: str = udspy.InputField() + context: dict[str, str] = udspy.InputField( + desc="A mapping of source URLs to content." + ) + + answer: str = udspy.OutputField() + sources: list[str] = udspy.OutputField( + desc=( + "A list of source URLs as strings used to generate the answer, " + "picked from the provided context keys, in order of importance." + ) + ) + reliability: float = udspy.OutputField( + desc=( + "The reliability score of the answer, from 0 to 1. " + "1 means the answer is fully supported by the provided context. " + "0 means the answer is not supported by the provided context." + ) + ) + + @classmethod + def format_context(cls, chunks: list[KnowledgeBaseChunk]) -> dict[str, str]: + """ + Formats the context as a list of strings for the signature. + Each string is formatted as "Source URL: content". + + :param chunks: The list of knowledge base chunks. + :return: A dictionary mapping source URLs to their combined content. + """ + + context = {} + for chunk in chunks: + url = chunk.source_document.source_url + content = chunk.content + if url not in context: + context[url] = content + else: + context[url] += "\n" + content + + return context + + +def get_search_user_docs_tool( + user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" +) -> Callable[[str], dict[str, Any]]: + """ + Returns a function that searches the Baserow documentation for a given query. + """ + + async def search_user_docs( + question: Annotated[ + str, "The English version of the user question, using Baserow vocabulary." + ] + ) -> dict[str, Any]: + """ + Search Baserow documentation to provide instructions and information for USERS. + + This tool provides end-user documentation explaining Baserow features and how + users can use them manually through the UI. It does NOT contain information + about: + - Which tools/functions the agent should use + - How to use agent tools or loaders + - Agent-specific implementation details + + Use this ONLY when the user explicitly asks for instructions on how to do + something themselves, or wants to learn about Baserow features. + + Make sure the question is in English and uses Baserow-specific terminology + to get the best results. + """ + + nonlocal tool_helpers + + tool_helpers.update_status(_("Exploring the knowledge base...")) + + @sync_to_async + def _search(question: str) -> list[KnowledgeBaseChunk]: + chunks = KnowledgeBaseHandler().search(question) + return list(chunks) + + searcher = udspy.ChainOfThought(SearchDocsSignature) + relevant_chunks = await _search(question) + prediction = await searcher.aexecute( + question=question, + context=SearchDocsSignature.format_context(relevant_chunks), + stream=True, + ) + + sources = [] + available_urls = {chunk.source_document.source_url for chunk in relevant_chunks} + for url in prediction.sources: + # somehow LLMs sometimes return sources as objects + if isinstance(url, dict) and "url" in url: + url = url["url"] + + if not isinstance(url, str): + continue + + if url in available_urls and url not in sources: + sources.append(url) + + # If for any reason the model wasn't able to return sources correctly, fill them + # from the available URLs. + if not sources: + sources = list(available_urls) + + return { + "answer": prediction.answer, + "reliability": prediction.reliability, + "sources": sources, + } + + return search_user_docs + + +class SearchDocsToolType(AssistantToolType): + type = "search_user_docs" + + def can_use( + self, user: AbstractUser, workspace: Workspace, *args, **kwargs + ) -> bool: + return KnowledgeBaseHandler().can_search() + + @classmethod + def get_tool( + cls, user: AbstractUser, workspace: Workspace, tool_helpers: "ToolHelpers" + ) -> Callable[[Any], Any]: + return get_search_user_docs_tool(user, workspace, tool_helpers) diff --git a/enterprise/backend/src/baserow_enterprise/assistant/types.py b/enterprise/backend/src/baserow_enterprise/assistant/types.py index 9d6337a055..8e4f068687 100644 --- a/enterprise/backend/src/baserow_enterprise/assistant/types.py +++ b/enterprise/backend/src/baserow_enterprise/assistant/types.py @@ -90,6 +90,9 @@ def from_validate_request(cls, request, ui_context_data) -> "UIContext": user_context = UserUIContext.from_user(request.user) return cls(user=user_context, **ui_context_data) + def format(self) -> dict: + return self.model_dump_json(exclude_none=True) + class AssistantMessageType(StrEnum): HUMAN = "human" diff --git a/enterprise/backend/src/baserow_enterprise/config/settings/settings.py b/enterprise/backend/src/baserow_enterprise/config/settings/settings.py index 02856687ff..63105b068d 100644 --- a/enterprise/backend/src/baserow_enterprise/config/settings/settings.py +++ b/enterprise/backend/src/baserow_enterprise/config/settings/settings.py @@ -80,3 +80,6 @@ def setup(settings): settings.BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL = os.getenv( "BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL", "" ) + settings.BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE = float( + os.getenv("BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE", "") or 0.3 + ) diff --git a/enterprise/backend/src/baserow_enterprise/locale/en/LC_MESSAGES/django.po b/enterprise/backend/src/baserow_enterprise/locale/en/LC_MESSAGES/django.po index f53de93b37..627e5114c3 100644 --- a/enterprise/backend/src/baserow_enterprise/locale/en/LC_MESSAGES/django.po +++ b/enterprise/backend/src/baserow_enterprise/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-17 14:14+0000\n" +"POT-Creation-Date: 2025-11-17 15:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,97 +18,146 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/baserow_enterprise/assistant/tools/database/tools.py:63 -msgid "Listing databases..." +#: src/baserow_enterprise/assistant/assistant.py:461 +msgid "Thinking..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:136 +#: src/baserow_enterprise/assistant/assistant.py:469 +msgid "" +"I wanted to search the documentation for you, but the search tool isn't " +"currently available.\n" +"\n" +"To enable documentation search, you'll need to set up the local knowledge " +"base. \n" +"\n" +"You can find setup instructions at: https://baserow.io/user-docs" +msgstr "" + +#: src/baserow_enterprise/assistant/tools/automation/tools.py:38 +msgid "Listing workflows..." +msgstr "" + +#: src/baserow_enterprise/assistant/tools/automation/utils.py:283 #, python-format -msgid "Listing tables in %(database_names)s..." +msgid "Creating workflow '%(name)s'..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:195 +#: src/baserow_enterprise/assistant/tools/automation/utils.py:296 #, python-format -msgid "Inspecting %(table_names)s schema..." +msgid "Creating trigger '%(label)s'..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:238 +#: src/baserow_enterprise/assistant/tools/automation/utils.py:316 #, python-format -msgid "Creating database %(database_name)s..." +msgid "Creating node '%(label)s'..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:303 +#: src/baserow_enterprise/assistant/tools/automation/utils.py:381 +#, python-format +msgid "Generating formulas for node '%(label)s'..." +msgstr "" + +#: src/baserow_enterprise/assistant/tools/core/tools.py:44 +#, python-format +msgid "Listing %(builder_types)ss..." +msgstr "" + +#: src/baserow_enterprise/assistant/tools/core/tools.py:103 +#, python-format +msgid "Creating %(builder_type)s %(builder_name)s..." +msgstr "" + +#: src/baserow_enterprise/assistant/tools/database/tools.py:92 +#, python-format +msgid "Listing tables in %(database_names)s..." +msgstr "" + +#: src/baserow_enterprise/assistant/tools/database/tools.py:151 +#, python-format +msgid "Inspecting %(table_names)s schema..." +msgstr "" + +#: src/baserow_enterprise/assistant/tools/database/tools.py:230 #, python-format msgid "Creating table %(table_name)s..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:346 +#: src/baserow_enterprise/assistant/tools/database/tools.py:272 msgid "Preparing example rows for these new tables..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:468 +#: src/baserow_enterprise/assistant/tools/database/tools.py:368 #, python-format msgid "Listing rows in %(table_name)s " msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:593 +#: src/baserow_enterprise/assistant/tools/database/tools.py:506 #, python-format msgid "Listing views in %(table_name)s..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:656 +#: src/baserow_enterprise/assistant/tools/database/tools.py:557 #, python-format -msgid "Creating %(view_type)s view %(view_name)s" +msgid "Creating filters in %(view_name)s..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/tools.py:723 +#: src/baserow_enterprise/assistant/tools/database/tools.py:602 #, python-format -msgid "Creating filters in %(view_name)s..." +msgid "Creating %(view_type)s view %(view_name)s" +msgstr "" + +#: src/baserow_enterprise/assistant/tools/database/tools.py:781 +msgid "Generating formula..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/utils.py:119 +#: src/baserow_enterprise/assistant/tools/database/utils.py:127 #, python-format msgid "Creating field %(field_name)s..." msgstr "" -#: src/baserow_enterprise/assistant/tools/database/utils.py:409 +#: src/baserow_enterprise/assistant/tools/database/utils.py:417 #, python-format msgid "Creating rows in %(table_name)s " msgstr "" -#: src/baserow_enterprise/assistant/tools/database/utils.py:450 +#: src/baserow_enterprise/assistant/tools/database/utils.py:458 #, python-format msgid "Updating rows in %(table_name)s " msgstr "" -#: src/baserow_enterprise/assistant/tools/database/utils.py:489 +#: src/baserow_enterprise/assistant/tools/database/utils.py:497 #, python-format msgid "Deleting rows in %(table_name)s " msgstr "" -#: src/baserow_enterprise/assistant/tools/navigation/tools.py:34 +#: src/baserow_enterprise/assistant/tools/navigation/tools.py:39 #, python-format msgid "Navigating to %(location)s..." msgstr "" -#: src/baserow_enterprise/assistant/tools/search_docs/tools.py:44 +#: src/baserow_enterprise/assistant/tools/search_user_docs/tools.py:102 msgid "Exploring the knowledge base..." msgstr "" -#: src/baserow_enterprise/assistant/types.py:176 +#: src/baserow_enterprise/assistant/types.py:220 #, python-format msgid "table %(table_name)s" msgstr "" -#: src/baserow_enterprise/assistant/types.py:187 +#: src/baserow_enterprise/assistant/types.py:232 #, python-format msgid "view %(view_name)s" msgstr "" -#: src/baserow_enterprise/assistant/types.py:194 +#: src/baserow_enterprise/assistant/types.py:239 msgid "home" msgstr "" +#: src/baserow_enterprise/assistant/types.py:249 +#, python-format +msgid "workflow %(workflow_name)s" +msgstr "" + #: src/baserow_enterprise/audit_log/job_types.py:36 msgid "User Email" msgstr "" diff --git a/enterprise/backend/src/baserow_enterprise/management/commands/sync_knowledge_base.py b/enterprise/backend/src/baserow_enterprise/management/commands/sync_knowledge_base.py index acb5d265c0..d2654191aa 100644 --- a/enterprise/backend/src/baserow_enterprise/management/commands/sync_knowledge_base.py +++ b/enterprise/backend/src/baserow_enterprise/management/commands/sync_knowledge_base.py @@ -1,6 +1,8 @@ from django.core.management.base import BaseCommand -from baserow_enterprise.assistant.tools.search_docs.handler import KnowledgeBaseHandler +from baserow_enterprise.assistant.tools.search_user_docs.handler import ( + KnowledgeBaseHandler, +) class Command(BaseCommand): diff --git a/enterprise/backend/tests/baserow_enterprise_tests/api/assistant/test_assistant_views.py b/enterprise/backend/tests/baserow_enterprise_tests/api/assistant/test_assistant_views.py index 43f19fe12c..87740e6f12 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/api/assistant/test_assistant_views.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/api/assistant/test_assistant_views.py @@ -220,10 +220,15 @@ def test_cannot_send_message_without_valid_workspace( @pytest.mark.django_db() @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_creates_chat_if_not_exists( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that sending a message creates a chat if it doesn't exist""" @@ -282,10 +287,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_streams_response( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that the endpoint streams AI responses properly""" @@ -865,10 +875,15 @@ def test_get_messages_includes_human_sentiment_when_feedback_exists( @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_streams_sources_from_tools( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that sources from tool calls are included in streamed responses""" @@ -954,10 +969,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_streams_thinking_messages_during_tool_execution( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that thinking messages are streamed during tool execution""" @@ -1039,10 +1059,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_generates_chat_title_on_first_message( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that a chat title is generated and streamed on the first message""" @@ -1109,10 +1134,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_does_not_generate_title_on_subsequent_messages( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that chat title is NOT regenerated on subsequent messages""" @@ -1179,10 +1209,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_handles_ai_error_in_streaming( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test that AI errors are properly streamed to the client""" @@ -1252,10 +1287,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_with_minimal_ui_context( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test sending message with minimal UI context (workspace only)""" @@ -1318,10 +1358,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_with_database_builder_context( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """ Test sending message with database builder context @@ -1398,10 +1443,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_with_application_builder_context( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """ Test sending message with application builder context @@ -1504,10 +1554,15 @@ def test_send_message_ui_context_validation_missing_workspace( @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_with_automation_context( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test sending message with automation builder context""" @@ -1575,10 +1630,15 @@ async def mock_astream(human_message): @pytest.mark.django_db @override_settings(DEBUG=True) +@patch("baserow_enterprise.api.assistant.views.check_lm_ready_or_raise") @patch("baserow_enterprise.assistant.handler.Assistant") @patch("baserow_enterprise.api.assistant.views.AssistantHandler") def test_send_message_with_dashboard_context( - mock_handler_class, mock_assistant_class, api_client, enterprise_data_fixture + mock_handler_class, + mock_assistant_class, + mock_check_lm, + api_client, + enterprise_data_fixture, ): """Test sending message with dashboard context""" diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant.py index 55eec28adc..7da32e04a5 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant.py @@ -8,7 +8,7 @@ - Generates and persists chat titles appropriately - Adapts its signature based on chat state """ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch from django.core.cache import cache @@ -83,7 +83,7 @@ def test_on_tool_end_extracts_sources_from_outputs(self): # Mock tool instance and inputs tool_instance = MagicMock() - tool_instance.name = "search_docs" + tool_instance.name = "search_user_docs" inputs = {"query": "test"} # Register tool call @@ -203,22 +203,31 @@ def test_aload_chat_history_formats_as_question_answer_pairs( ) assistant = Assistant(chat) - async_to_sync(assistant.aload_chat_history)() + assistant.history = async_to_sync(assistant.afetch_chat_history)() # History should contain user/assistant message pairs assert assistant.history is not None - assert len(assistant.history) == 4 + assert len(assistant.history.messages) == 4 # First pair - assert assistant.history[0] == "Human: What is Baserow?" - assert assistant.history[1] == "AI: Baserow is a no-code database platform." + assert assistant.history.messages[0] == { + "role": "user", + "content": "What is Baserow?", + } + assert assistant.history.messages[1] == { + "role": "assistant", + "content": "Baserow is a no-code database platform.", + } # Second pair - assert assistant.history[2] == "Human: How do I create a table?" - assert ( - assistant.history[3] - == "AI: You can create a table by clicking the + button." - ) + assert assistant.history.messages[2] == { + "role": "user", + "content": "How do I create a table?", + } + assert assistant.history.messages[3] == { + "role": "assistant", + "content": "You can create a table by clicking the + button.", + } def test_aload_chat_history_respects_limit(self, enterprise_data_fixture): """Test that history loading respects the limit parameter""" @@ -241,10 +250,12 @@ def test_aload_chat_history_respects_limit(self, enterprise_data_fixture): ) assistant = Assistant(chat) - async_to_sync(assistant.aload_chat_history)(limit=6) # Last 6 messages + assistant.history = async_to_sync(assistant.afetch_chat_history)( + limit=6 + ) # Last 6 messages # Should only load the most recent 6 messages (3 pairs) - assert len(assistant.history) == 6 + assert len(assistant.history.messages) == 6 def test_aload_chat_history_handles_incomplete_pairs(self, enterprise_data_fixture): """ @@ -271,22 +282,117 @@ def test_aload_chat_history_handles_incomplete_pairs(self, enterprise_data_fixtu ) assistant = Assistant(chat) - async_to_sync(assistant.aload_chat_history)() + assistant.history = async_to_sync(assistant.afetch_chat_history)() # Should only include the complete pair (2 messages: user + assistant) - assert len(assistant.history) == 2 - assert assistant.history[0] == "Human: Question 1" - assert assistant.history[1] == "AI: Answer 1" + assert len(assistant.history.messages) == 2 + assert assistant.history.messages[0] == { + "role": "user", + "content": "Question 1", + } + assert assistant.history.messages[1] == { + "role": "assistant", + "content": "Answer 1", + } + + @patch("udspy.ReAct.astream") + @patch("udspy.LM") + def test_history_is_passed_to_astream_as_context( + self, mock_lm, mock_react_astream, enterprise_data_fixture + ): + """ + Test that chat history is loaded correctly and passed to the agent as context + """ + + user = enterprise_data_fixture.create_user() + workspace = enterprise_data_fixture.create_workspace(user=user) + chat = AssistantChat.objects.create( + user=user, workspace=workspace, title="Test Chat" + ) + + # Create conversation history (2 complete pairs) + AssistantChatMessage.objects.create( + chat=chat, role=AssistantChatMessage.Role.HUMAN, content="What is Baserow?" + ) + AssistantChatMessage.objects.create( + chat=chat, + role=AssistantChatMessage.Role.AI, + content="Baserow is a no-code database", + ) + AssistantChatMessage.objects.create( + chat=chat, + role=AssistantChatMessage.Role.HUMAN, + content="How do I create a table?", + ) + AssistantChatMessage.objects.create( + chat=chat, + role=AssistantChatMessage.Role.AI, + content="Click the Create Table button", + ) + + assistant = Assistant(chat) + + # Mock the router stream to delegate to agent with extracted context + def mock_router_stream_factory(*args, **kwargs): + # Verify conversation history is passed to router + assert kwargs["conversation_history"] == [ + "[0] (user): What is Baserow?", + "[1] (assistant): Baserow is a no-code database", + "[2] (user): How do I create a table?", + "[3] (assistant): Click the Create Table button", + ] + + async def _stream(): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="User wants to add a view to their table", + search_query="", + ) + + return _stream() + + # Patch the instance method + assistant._request_router.astream = Mock(side_effect=mock_router_stream_factory) + + # Mock the agent stream + def mock_agent_stream_factory(*args, **kwargs): + # Verify extracted context is passed to agent + assert kwargs["context"] == "User wants to add a view to their table" + + async def _stream(): + yield OutputStreamChunk( + module=None, + field_name="answer", + delta="Answer", + content="Answer", + is_complete=False, + ) + yield Prediction(answer="Answer", trajectory=[], reasoning="") + + return _stream() + + mock_react_astream.side_effect = mock_agent_stream_factory + mock_lm.return_value.model = "test-model" + + message = HumanMessage(content="How to add a view?") + + # Consume the stream to trigger assertions + async def consume_stream(): + async for _ in assistant.astream_messages(message): + pass + + async_to_sync(consume_stream)() @pytest.mark.django_db class TestAssistantMessagePersistence: """Test that messages are persisted correctly during streaming""" + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.LM") def test_astream_messages_persists_human_message( - self, mock_lm, mock_astream, enterprise_data_fixture + self, mock_lm, mock_react_astream, mock_cot_astream, enterprise_data_fixture ): """Test that human messages are persisted to database before streaming""" @@ -296,8 +402,18 @@ def test_astream_messages_persists_human_message( user=user, workspace=workspace, title="Test Chat" ) - # Mock the streaming - async def mock_stream(*args, **kwargs): + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + # Mock the agent streaming + async def mock_agent_stream(*args, **kwargs): # Yield a simple response yield OutputStreamChunk( module=None, @@ -308,7 +424,7 @@ async def mock_stream(*args, **kwargs): ) yield Prediction(answer="Hello", trajectory=[], reasoning="") - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() # Configure mock LM to return a serializable model name mock_lm.return_value.model = "test-model" @@ -338,10 +454,11 @@ async def consume_stream(): ).first() assert saved_message.content == "Test message" + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.LM") def test_astream_messages_persists_ai_message_with_sources( - self, mock_lm, mock_astream, enterprise_data_fixture + self, mock_lm, mock_react_astream, mock_cot_astream, enterprise_data_fixture ): """Test that AI messages are persisted with sources in artifacts""" @@ -356,8 +473,18 @@ def test_astream_messages_persists_ai_message_with_sources( assistant = Assistant(chat) - # Mock the streaming with a Prediction at the end - async def mock_stream(*args, **kwargs): + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + # Mock the agent streaming with a Prediction at the end + async def mock_agent_stream(*args, **kwargs): yield OutputStreamChunk( module=None, field_name="answer", @@ -372,7 +499,7 @@ async def mock_stream(*args, **kwargs): reasoning="", ) - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() ui_context = UIContext( workspace=WorkspaceUIContext(id=workspace.id, name=workspace.name), user=UserUIContext(id=user.id, name=user.first_name, email=user.email), @@ -394,12 +521,14 @@ async def consume_stream(): ).count() assert ai_messages == 1 + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.Predict") def test_astream_messages_persists_chat_title( self, mock_predict_class, - mock_astream, + mock_react_astream, + mock_cot_astream, enterprise_data_fixture, ): """Test that chat titles are persisted to the database""" @@ -420,8 +549,18 @@ async def mock_title_aforward(*args, **kwargs): assistant = Assistant(chat) - # Mock streaming - async def mock_stream(*args, **kwargs): + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + # Mock agent streaming + async def mock_agent_stream(*args, **kwargs): yield OutputStreamChunk( module=None, field_name="answer", @@ -433,7 +572,7 @@ async def mock_stream(*args, **kwargs): module=assistant._assistant, answer="Hello", trajectory=[], reasoning="" ) - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() ui_context = UIContext( workspace=WorkspaceUIContext(id=workspace.id, name=workspace.name), user=UserUIContext(id=user.id, name=user.first_name, email=user.email), @@ -458,10 +597,11 @@ async def consume_stream(): class TestAssistantStreaming: """Test streaming behavior of the Assistant""" + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.LM") def test_astream_messages_yields_answer_chunks( - self, mock_lm, mock_astream, enterprise_data_fixture + self, mock_lm, mock_react_astream, mock_cot_astream, enterprise_data_fixture ): """Test that answer chunks are yielded during streaming""" @@ -471,17 +611,29 @@ def test_astream_messages_yields_answer_chunks( user=user, workspace=workspace, title="Test Chat" ) - # Mock streaming - async def mock_stream(*args, **kwargs): + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + assistant = Assistant(chat) + + # Mock agent streaming + async def mock_agent_stream(*args, **kwargs): yield OutputStreamChunk( - module=None, + module=assistant._assistant.extract_module, field_name="answer", delta="Hello", content="Hello", is_complete=False, ) yield OutputStreamChunk( - module=None, + module=assistant._assistant.extract_module, field_name="answer", delta=" world", content="Hello world", @@ -489,20 +641,14 @@ async def mock_stream(*args, **kwargs): ) yield Prediction(answer="Hello world", trajectory=[], reasoning="") - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() # Configure mock LM to return a serializable model name mock_lm.return_value.model = "test-model" - assistant = Assistant(chat) - ui_context = UIContext( - workspace=WorkspaceUIContext(id=workspace.id, name=workspace.name), - user=UserUIContext(id=user.id, name=user.first_name, email=user.email), - ) - async def consume_stream(): chunks = [] - human_message = HumanMessage(content="Test", ui_context=ui_context) + human_message = HumanMessage(content="Test") async for msg in assistant.astream_messages(human_message): if isinstance(msg, AiMessageChunk): chunks.append(msg) @@ -515,12 +661,14 @@ async def consume_stream(): assert chunks[0].content == "Hello" assert chunks[1].content == "Hello world" + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.Predict") def test_astream_messages_yields_title_chunks( self, mock_predict_class, - mock_astream, + mock_react_astream, + mock_cot_astream, enterprise_data_fixture, ): """Test that title chunks are yielded for new chats""" @@ -541,8 +689,18 @@ async def mock_title_aforward(*args, **kwargs): assistant = Assistant(chat) - # Mock streaming - async def mock_stream(*args, **kwargs): + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + # Mock agent streaming + async def mock_agent_stream(*args, **kwargs): yield OutputStreamChunk( module=None, field_name="answer", @@ -557,15 +715,11 @@ async def mock_stream(*args, **kwargs): reasoning="", ) - mock_astream.return_value = mock_stream() - ui_context = UIContext( - workspace=WorkspaceUIContext(id=workspace.id, name=workspace.name), - user=UserUIContext(id=user.id, name=user.first_name, email=user.email), - ) + mock_react_astream.return_value = mock_agent_stream() async def consume_stream(): title_messages = [] - human_message = HumanMessage(content="Test", ui_context=ui_context) + human_message = HumanMessage(content="Test") async for msg in assistant.astream_messages(human_message): if isinstance(msg, ChatTitleMessage): title_messages.append(msg) @@ -577,10 +731,11 @@ async def consume_stream(): assert len(title_messages) == 1 assert title_messages[0].content == "Title" + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.LM") def test_astream_messages_yields_thinking_messages( - self, mock_lm, mock_astream, enterprise_data_fixture + self, mock_lm, mock_react_astream, mock_cot_astream, enterprise_data_fixture ): """Test that thinking messages from tools are yielded""" @@ -590,11 +745,23 @@ def test_astream_messages_yields_thinking_messages( user=user, workspace=workspace, title="Test Chat" ) - # Mock streaming - async def mock_stream(*args, **kwargs): - yield AiThinkingMessage(content="thinking") + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + assistant = Assistant(chat) + + # Mock the agent streaming + async def mock_agent_stream(*args, **kwargs): + yield AiThinkingMessage(content="still thinking...") yield OutputStreamChunk( - module=None, + module=assistant._assistant.extract_module, field_name="answer", delta="Answer", content="Answer", @@ -602,12 +769,11 @@ async def mock_stream(*args, **kwargs): ) yield Prediction(answer="Answer", trajectory=[], reasoning="") - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() # Configure mock LM to return a serializable model name mock_lm.return_value.model = "test-model" - assistant = Assistant(chat) ui_context = UIContext( workspace=WorkspaceUIContext(id=workspace.id, name=workspace.name), user=UserUIContext(id=user.id, name=user.first_name, email=user.email), @@ -624,8 +790,9 @@ async def consume_stream(): thinking_messages = async_to_sync(consume_stream)() # Should receive thinking messages - assert len(thinking_messages) == 1 - assert thinking_messages[0].content == "thinking" + assert len(thinking_messages) == 2 + assert thinking_messages[0].content == "Thinking..." + assert thinking_messages[1].content == "still thinking..." @pytest.mark.django_db @@ -887,10 +1054,11 @@ def test_check_cancellation_does_nothing_when_no_flag( # Should not raise assistant._check_cancellation(cache_key, "msg123") + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.LM") def test_astream_messages_yields_ai_started_message( - self, mock_lm, mock_astream, enterprise_data_fixture + self, mock_lm, mock_react_astream, mock_cot_astream, enterprise_data_fixture ): """Test that astream_messages yields AiStartedMessage at the beginning""" @@ -900,8 +1068,18 @@ def test_astream_messages_yields_ai_started_message( user=user, workspace=workspace, title="Test" ) - # Mock the streaming - async def mock_stream(*args, **kwargs): + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + + # Mock the agent streaming + async def mock_agent_stream(*args, **kwargs): yield OutputStreamChunk( module=None, field_name="answer", @@ -911,15 +1089,11 @@ async def mock_stream(*args, **kwargs): ) yield Prediction(answer="Hello there!", trajectory=[], reasoning="") - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() mock_lm.return_value.model = "test-model" assistant = Assistant(chat) - ui_context = UIContext( - workspace=WorkspaceUIContext(id=workspace.id, name=workspace.name), - user=UserUIContext(id=user.id, name=user.first_name, email=user.email), - ) - human_message = HumanMessage(content="Hello", ui_context=ui_context) + human_message = HumanMessage(content="Hello") # Collect messages async def collect_messages(): @@ -935,10 +1109,11 @@ async def collect_messages(): assert isinstance(messages[0], AiStartedMessage) assert messages[0].message_id is not None + @patch("udspy.ChainOfThought.astream") @patch("udspy.ReAct.astream") @patch("udspy.LM") def test_astream_messages_checks_cancellation_periodically( - self, mock_lm, mock_astream, enterprise_data_fixture + self, mock_lm, mock_react_astream, mock_cot_astream, enterprise_data_fixture ): """Test that astream_messages checks for cancellation every 10 chunks""" @@ -948,8 +1123,18 @@ def test_astream_messages_checks_cancellation_periodically( user=user, workspace=workspace, title="Test" ) + # Mock the router stream + async def mock_router_stream(*args, **kwargs): + yield Prediction( + routing_decision="delegate_to_agent", + extracted_context="", + search_query="", + ) + + mock_cot_astream.return_value = mock_router_stream() + # Mock the stream to return many chunks - enough to trigger check at 10 - async def mock_stream(*args, **kwargs): + async def mock_agent_stream(*args, **kwargs): # Yield 15 chunks - cancellation check happens at chunk 10 for i in range(15): yield OutputStreamChunk( @@ -961,7 +1146,7 @@ async def mock_stream(*args, **kwargs): ) yield Prediction(answer="Complete response", trajectory=[], reasoning="") - mock_astream.return_value = mock_stream() + mock_react_astream.return_value = mock_agent_stream() mock_lm.return_value.model = "test-model" assistant = Assistant(chat) diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_automation_workflow_tools.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_automation_workflow_tools.py index c0138b7e2f..1e8ee8f8b0 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_automation_workflow_tools.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_automation_workflow_tools.py @@ -1,12 +1,15 @@ +from unittest.mock import Mock + import pytest +from udspy.module.callbacks import ModuleContext, is_module_callback from baserow.contrib.automation.workflows.handler import AutomationWorkflowHandler from baserow.core.formula import resolve_formula from baserow.core.formula.registries import formula_runtime_function_registry from baserow.core.formula.types import BASEROW_FORMULA_MODE_ADVANCED from baserow_enterprise.assistant.tools.automation.tools import ( - get_create_workflows_tool, get_list_workflows_tool, + get_workflow_tool_factory, ) from baserow_enterprise.assistant.tools.automation.types import ( CreateRowActionCreate, @@ -94,8 +97,25 @@ def test_create_workflows(data_fixture): database = data_fixture.create_database_application(user=user, workspace=workspace) table = data_fixture.create_database_table(user=user, database=database) - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( @@ -143,8 +163,25 @@ def test_create_multiple_workflows(data_fixture): database = data_fixture.create_database_application(user=user, workspace=workspace) table = data_fixture.create_database_table(user=user, database=database) - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( @@ -248,8 +285,25 @@ def test_create_workflow_with_row_triggers_and_actions(data_fixture, trigger, ac table.pk = 999 # To match the action's table_id table.save() - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( @@ -286,8 +340,25 @@ def test_create_row_action_with_field_ids(data_fixture): text_field = data_fixture.create_text_field(table=table, name="Name") number_field = data_fixture.create_number_field(table=table, name="Age") - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( @@ -340,8 +411,25 @@ def test_update_row_action_with_row_id_and_field_ids(data_fixture): table = data_fixture.create_database_table(user=user, database=database) text_field = data_fixture.create_text_field(table=table, name="Status") - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( @@ -395,8 +483,25 @@ def test_delete_row_action_with_row_id(data_fixture): database = data_fixture.create_database_application(user=user, workspace=workspace) table = data_fixture.create_database_table(user=user, database=database) - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( @@ -449,8 +554,25 @@ def test_router_node_with_required_conditions(data_fixture): database = data_fixture.create_database_application(user=user, workspace=workspace) table = data_fixture.create_database_table(user=user, database=database) - tool = get_create_workflows_tool(user, workspace, fake_tool_helpers) - result = tool( + factory = get_workflow_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_workflows_tool = next( + (tool for tool in added_tools if tool.name == "create_workflows"), None + ) + assert create_workflows_tool is not None + + result = create_workflows_tool.func( automation_id=automation.id, workflows=[ WorkflowCreate( diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_rows_tools.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_rows_tools.py index 4b83e36489..004477d9e6 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_rows_tools.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_rows_tools.py @@ -6,7 +6,7 @@ from baserow.contrib.database.rows.handler import RowHandler from baserow_enterprise.assistant.tools.database.tools import ( get_list_rows_tool, - get_rows_meta_tool, + get_rows_tools_factory, ) from .utils import fake_tool_helpers @@ -211,7 +211,7 @@ def test_create_rows(data_fixture): table = res["table_a"] tool_helpers = fake_tool_helpers - meta_tool = get_rows_meta_tool(user, workspace, tool_helpers) + meta_tool = get_rows_tools_factory(user, workspace, tool_helpers) assert callable(meta_tool) tools_upgrade = meta_tool([table.id], ["create"]) @@ -277,7 +277,7 @@ def test_update_rows(data_fixture): table = res["table_a"] tool_helpers = fake_tool_helpers - meta_tool = get_rows_meta_tool(user, workspace, tool_helpers) + meta_tool = get_rows_tools_factory(user, workspace, tool_helpers) assert callable(meta_tool) tools_upgrade = meta_tool([table.id], ["update"]) assert is_module_callback(tools_upgrade) @@ -291,7 +291,7 @@ def test_update_rows(data_fixture): added_tools = mock_module.init_module.call_args[1]["tools"] added_tools_names = [tool.name for tool in added_tools] assert len(added_tools) == 1 - assert f"update_rows_in_table_{table.id}_by_row_ids" in added_tools_names + assert f"update_rows_in_table_{table.id}" in added_tools_names table_model = table.get_model() assert table_model.objects.count() == 3 @@ -371,7 +371,7 @@ def test_delete_rows(data_fixture): table = res["table_a"] tool_helpers = fake_tool_helpers - meta_tool = get_rows_meta_tool(user, workspace, tool_helpers) + meta_tool = get_rows_tools_factory(user, workspace, tool_helpers) assert callable(meta_tool) tools_upgrade = meta_tool([table.id], ["delete"]) @@ -384,7 +384,7 @@ def test_delete_rows(data_fixture): added_tools = mock_module.init_module.call_args[1]["tools"] added_tools_names = [tool.name for tool in added_tools] assert len(added_tools) == 1 - assert f"delete_rows_in_table_{table.id}_by_row_ids" in added_tools_names + assert f"delete_rows_in_table_{table.id}" in added_tools_names delete_table_rows = added_tools[0] table_model = table.get_model() diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_table_tools.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_table_tools.py index d065bdefac..67c0786ec1 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_table_tools.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_table_tools.py @@ -1,15 +1,16 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest +from udspy.module.callbacks import ModuleContext, is_module_callback from baserow.contrib.database.fields.models import FormulaField from baserow.contrib.database.formula.registries import formula_function_registry from baserow.contrib.database.table.models import Table from baserow.test_utils.helpers import AnyInt from baserow_enterprise.assistant.tools.database.tools import ( - get_create_tables_tool, get_generate_database_formula_tool, get_list_tables_tool, + get_table_and_fields_tools_factory, ) from baserow_enterprise.assistant.tools.database.types import ( BooleanFieldItemCreate, @@ -187,8 +188,29 @@ def test_create_simple_table_tool(data_fixture): workspace=workspace, name="Database 1" ) - tool = get_create_tables_tool(user, workspace, fake_tool_helpers) - response = tool( + factory = get_table_and_fields_tools_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + assert len(added_tools) == 2 # create_tables and create_fields + + # Find the create_tables tool + create_tables_tool = next( + (tool for tool in added_tools if tool.name == "create_tables"), None + ) + assert create_tables_tool is not None + + # Call the underlying function directly (not through udspy.Tool wrapper) + response = create_tables_tool.func( database_id=database.id, tables=[ TableItemCreate( @@ -220,7 +242,27 @@ def test_create_complex_table_tool(data_fixture): ) table = data_fixture.create_database_table(database=database, name="Table 1") - tool = get_create_tables_tool(user, workspace, fake_tool_helpers) + factory = get_table_and_fields_tools_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + assert len(added_tools) == 2 # create_tables and create_fields + + # Find the create_tables tool + create_tables_tool = next( + (tool for tool in added_tools if tool.name == "create_tables"), None + ) + assert create_tables_tool is not None + primary_field = TextFieldItemCreate(type="text", name="Name") fields = [ LongTextFieldItemCreate( @@ -282,7 +324,8 @@ def test_create_complex_table_tool(data_fixture): name="Attachments", ), ] - response = tool( + # Call the underlying function directly (not through udspy.Tool wrapper) + response = create_tables_tool.func( database_id=database.id, tables=[ TableItemCreate( diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_views_tools.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_views_tools.py index 25ec2b3a16..bf3e940d08 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_views_tools.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_database_views_tools.py @@ -1,10 +1,12 @@ +from unittest.mock import Mock + import pytest +from udspy.module.callbacks import ModuleContext, is_module_callback from baserow.contrib.database.views.models import View, ViewFilter from baserow_enterprise.assistant.tools.database.tools import ( - get_create_view_filters_tool, - get_create_views_tool, get_list_views_tool, + get_views_tool_factory, ) from baserow_enterprise.assistant.tools.database.types import ( BooleanIsViewFilterItemCreate, @@ -40,6 +42,52 @@ from .utils import fake_tool_helpers +def get_create_views_tool(user, workspace): + """Helper to get the create_views tool from the factory""" + + factory = get_views_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_views_tool = next( + (tool for tool in added_tools if tool.name == "create_views"), None + ) + assert create_views_tool is not None + return create_views_tool + + +def get_create_view_filters_tool(user, workspace): + """Helper to get the create_view_filters tool from the factory""" + + factory = get_views_tool_factory(user, workspace, fake_tool_helpers) + assert callable(factory) + + tools_upgrade = factory() + assert is_module_callback(tools_upgrade) + + mock_module = Mock() + mock_module._tools = [] + mock_module.init_module = Mock() + tools_upgrade(ModuleContext(module=mock_module)) + assert mock_module.init_module.called + + added_tools = mock_module.init_module.call_args[1]["tools"] + create_filters_tool = next( + (tool for tool in added_tools if tool.name == "create_view_filters"), None + ) + assert create_filters_tool is not None + return create_filters_tool + + @pytest.mark.django_db def test_list_views_tool(data_fixture): user = data_fixture.create_user() @@ -77,8 +125,8 @@ def test_create_grid_view(data_fixture): database = data_fixture.create_database_application(workspace=workspace) table = data_fixture.create_database_table(database=database) - tool = get_create_views_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_views_tool(user, workspace) + response = tool.func( table_id=table.id, views=[ GridViewItemCreate( @@ -100,8 +148,8 @@ def test_create_kanban_view(data_fixture): table = data_fixture.create_database_table(database=database) single_select = data_fixture.create_single_select_field(table=table, name="Status") - tool = get_create_views_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_views_tool(user, workspace) + response = tool.func( table_id=table.id, views=[ KanbanViewItemCreate( @@ -126,8 +174,8 @@ def test_create_calendar_view(data_fixture): table = data_fixture.create_database_table(database=database) date_field = data_fixture.create_date_field(table=table, name="Date") - tool = get_create_views_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_views_tool(user, workspace) + response = tool.func( table_id=table.id, views=[ CalendarViewItemCreate( @@ -152,8 +200,8 @@ def test_create_gallery_view(data_fixture): table = data_fixture.create_database_table(database=database) file_field = data_fixture.create_file_field(table=table, name="Files") - tool = get_create_views_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_views_tool(user, workspace) + response = tool.func( table_id=table.id, views=[ GalleryViewItemCreate( @@ -179,8 +227,8 @@ def test_create_timeline_view(data_fixture): start_date = data_fixture.create_date_field(table=table, name="Start Date") end_date = data_fixture.create_date_field(table=table, name="End Date") - tool = get_create_views_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_views_tool(user, workspace) + response = tool.func( table_id=table.id, views=[ TimelineViewItemCreate( @@ -206,8 +254,8 @@ def test_create_form_view(data_fixture): table = data_fixture.create_database_table(database=database) field = data_fixture.create_text_field(table=table, name="Name", primary=True) - tool = get_create_views_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_views_tool(user, workspace) + response = tool.func( table_id=table.id, views=[ FormViewItemCreate( @@ -249,8 +297,8 @@ def test_create_text_equal_filter(data_fixture): field = data_fixture.create_text_field(table=table, name="Name") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -278,8 +326,8 @@ def test_create_text_not_equal_filter(data_fixture): field = data_fixture.create_text_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -291,7 +339,7 @@ def test_create_text_not_equal_filter(data_fixture): value="test", ) ], - ) + ), ] ) @@ -308,8 +356,8 @@ def test_create_text_contains_filter(data_fixture): field = data_fixture.create_text_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -321,7 +369,7 @@ def test_create_text_contains_filter(data_fixture): value="test", ) ], - ) + ), ] ) @@ -338,8 +386,8 @@ def test_create_text_not_contains_filter(data_fixture): field = data_fixture.create_text_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -351,7 +399,7 @@ def test_create_text_not_contains_filter(data_fixture): value="test", ) ], - ) + ), ] ) @@ -371,8 +419,8 @@ def test_create_number_equal_filter(data_fixture): field = data_fixture.create_number_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -381,7 +429,7 @@ def test_create_number_equal_filter(data_fixture): field_id=field.id, type="number", operator="equal", value=42.0 ) ], - ) + ), ] ) @@ -398,8 +446,8 @@ def test_create_number_not_equal_filter(data_fixture): field = data_fixture.create_number_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -411,7 +459,7 @@ def test_create_number_not_equal_filter(data_fixture): value=42.0, ) ], - ) + ), ] ) @@ -428,8 +476,8 @@ def test_create_number_higher_than_filter(data_fixture): field = data_fixture.create_number_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -443,7 +491,7 @@ def test_create_number_higher_than_filter(data_fixture): ) ], ) - ] + ], ) assert len(response["created_view_filters"]) == 1 @@ -461,8 +509,8 @@ def test_create_number_lower_than_filter(data_fixture): field = data_fixture.create_number_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -475,7 +523,7 @@ def test_create_number_lower_than_filter(data_fixture): or_equal=False, ) ], - ) + ), ] ) @@ -493,8 +541,8 @@ def test_create_date_equal_filter(data_fixture): field = data_fixture.create_date_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -507,7 +555,7 @@ def test_create_date_equal_filter(data_fixture): mode="exact_date", ) ], - ) + ), ] ) @@ -524,8 +572,8 @@ def test_create_date_not_equal_filter(data_fixture): field = data_fixture.create_date_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -538,7 +586,7 @@ def test_create_date_not_equal_filter(data_fixture): mode="today", ) ], - ) + ), ] ) @@ -557,8 +605,8 @@ def test_create_date_after_filter(data_fixture): field = data_fixture.create_date_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -572,7 +620,7 @@ def test_create_date_after_filter(data_fixture): or_equal=False, ) ], - ) + ), ] ) @@ -591,8 +639,8 @@ def test_create_date_before_filter(data_fixture): field = data_fixture.create_date_field(table=table) view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -606,7 +654,7 @@ def test_create_date_before_filter(data_fixture): or_equal=True, ) ], - ) + ), ] ) @@ -628,8 +676,8 @@ def test_create_single_select_is_any_of_filter(data_fixture): data_fixture.create_select_option(field=field, value="Option 2") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -641,7 +689,7 @@ def test_create_single_select_is_any_of_filter(data_fixture): value=["Option 1", "Option 2"], ) ], - ) + ), ] ) @@ -661,8 +709,8 @@ def test_create_single_select_is_none_of_filter(data_fixture): data_fixture.create_select_option(field=field, value="Bad Option") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -674,7 +722,7 @@ def test_create_single_select_is_none_of_filter(data_fixture): value=["Bad Option"], ) ], - ) + ), ] ) @@ -694,8 +742,8 @@ def test_create_boolean_is_true_filter(data_fixture): field = data_fixture.create_boolean_field(table=table, name="Active") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -704,7 +752,7 @@ def test_create_boolean_is_true_filter(data_fixture): field_id=field.id, type="boolean", operator="is", value=True ) ], - ) + ), ] ) @@ -721,8 +769,8 @@ def test_create_boolean_is_false_filter(data_fixture): field = data_fixture.create_boolean_field(table=table, name="Active") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -731,7 +779,7 @@ def test_create_boolean_is_false_filter(data_fixture): field_id=field.id, type="boolean", operator="is", value=False ) ], - ) + ), ] ) @@ -751,8 +799,8 @@ def test_create_multiple_select_is_any_of_filter(data_fixture): data_fixture.create_select_option(field=field, value="Tag 2") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -764,7 +812,7 @@ def test_create_multiple_select_is_any_of_filter(data_fixture): value=["Tag 1", "Tag 2"], ) ], - ) + ), ] ) @@ -784,8 +832,8 @@ def test_create_multiple_select_is_none_of_filter(data_fixture): data_fixture.create_select_option(field=field, value="Bad Tag") view = data_fixture.create_grid_view(table=table) - tool = get_create_view_filters_tool(user, workspace, fake_tool_helpers) - response = tool( + tool = get_create_view_filters_tool(user, workspace) + response = tool.func( [ ViewFiltersArgs( view_id=view.id, @@ -797,10 +845,9 @@ def test_create_multiple_select_is_none_of_filter(data_fixture): value=["Bad Tag"], ) ], - ) + ), ] ) - assert len(response["created_view_filters"]) == 1 assert ViewFilter.objects.filter( view=view, field=field, type="multiple_select_has_not" diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_knowledge_retrieval_handler.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_knowledge_retrieval_handler.py index 7297760d40..97d62c37ef 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_knowledge_retrieval_handler.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant_knowledge_retrieval_handler.py @@ -9,7 +9,7 @@ KnowledgeBaseChunk, KnowledgeBaseDocument, ) -from baserow_enterprise.assistant.tools.search_docs.handler import ( +from baserow_enterprise.assistant.tools.search_user_docs.handler import ( BaserowEmbedder, KnowledgeBaseHandler, VectorHandler, @@ -27,7 +27,7 @@ def test_returns_list_of_vectors(self): # Mock the httpxClient where it's used in the handler module with patch( - "baserow_enterprise.assistant.tools.search_docs.handler.httpxClient" + "baserow_enterprise.assistant.tools.search_user_docs.handler.httpxClient" ) as mock_client: mock_client_instance = mock_client.return_value mock_post_response = mock_client_instance.post.return_value @@ -56,7 +56,7 @@ def test_returns_embeddings_with_correct_dimensions(self): # Mock the httpxClient where it's used in the handler module with patch( - "baserow_enterprise.assistant.tools.search_docs.handler.httpxClient" + "baserow_enterprise.assistant.tools.search_user_docs.handler.httpxClient" ) as mock_client: mock_client_instance = mock_client.return_value mock_post_response = mock_client_instance.post.return_value @@ -82,7 +82,7 @@ def test_pads_smaller_dimensions_with_zeros(self): # Mock the httpxClient where it's used in the handler module with patch( - "baserow_enterprise.assistant.tools.search_docs.handler.httpxClient" + "baserow_enterprise.assistant.tools.search_user_docs.handler.httpxClient" ) as mock_client: # Mock the httpxClient.post call with smaller dimensions small_dimension = 512 @@ -111,7 +111,7 @@ def test_raises_error_on_larger_dimensions(self): # Mock the httpxClient where it's used in the handler module with patch( - "baserow_enterprise.assistant.tools.search_docs.handler.httpxClient" + "baserow_enterprise.assistant.tools.search_user_docs.handler.httpxClient" ) as mock_client: # Mock the httpxClient.post call with larger dimensions large_dimension = DEFAULT_EMBEDDING_DIMENSIONS + 100 @@ -255,7 +255,7 @@ def test_retrieve_knowledge_chunks_empty_store(self, knowledge_handler): """Test knowledge retrieval when vector store is empty""" results = knowledge_handler.search("database query") - assert results == [] + assert list(results) == [] def test_retrieve_knowledge_chunks_with_data( self, knowledge_handler, sample_documents_with_chunks @@ -267,7 +267,10 @@ def test_retrieve_knowledge_chunks_with_data( # The chunks are already in the database and available for search # Query for database-related content - results = knowledge_handler.search("database fundamentals", num_results=5) + results = [ + ch.content + for ch in knowledge_handler.search("database fundamentals", num_results=5) + ] assert len(results) > 0 assert any( @@ -352,7 +355,9 @@ def test_search_orders_by_l2_distance(self, knowledge_handler): # Search with a query that will be embedded as [1.0, 0.0, 0.0, ...] # (our MockEmbeddings returns this for "database" queries) - results = knowledge_handler.search("database", num_results=3) + results = [ + ch.content for ch in knowledge_handler.search("database", num_results=3) + ] # Results should be ordered by distance (closest first) assert len(results) == 3 @@ -405,7 +410,9 @@ def test_search_l2_distance_with_different_vectors(self, knowledge_handler): index=2, ) - results = knowledge_handler.search("database", num_results=3) + results = [ + ch.content for ch in knowledge_handler.search("database", num_results=3) + ] # Should be ordered: distance 0, sqrt(0.5), sqrt(2) assert len(results) == 3 @@ -491,7 +498,7 @@ def test_handler_with_default_vector_store(self): """Test handler creation with default vector store""" with patch( - "baserow_enterprise.assistant.tools.search_docs.handler.VectorHandler" + "baserow_enterprise.assistant.tools.search_user_docs.handler.VectorHandler" ) as mock_vector_handler: handler = KnowledgeBaseHandler() diff --git a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_sync_knowledge_base.py b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_sync_knowledge_base.py index e6eddb0e28..e89edee9fc 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_sync_knowledge_base.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/assistant/test_sync_knowledge_base.py @@ -10,7 +10,9 @@ KnowledgeBaseChunk, KnowledgeBaseDocument, ) -from baserow_enterprise.assistant.tools.search_docs.handler import KnowledgeBaseHandler +from baserow_enterprise.assistant.tools.search_user_docs.handler import ( + KnowledgeBaseHandler, +) @pytest.fixture diff --git a/enterprise/web-frontend/modules/baserow_enterprise/plugins.js b/enterprise/web-frontend/modules/baserow_enterprise/plugins.js index ae444aebe7..6a7cc65440 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/plugins.js +++ b/enterprise/web-frontend/modules/baserow_enterprise/plugins.js @@ -14,7 +14,6 @@ import AssistantSidebarItem from '@baserow_enterprise/components/assistant/Assis import AssistantPanel from '@baserow_enterprise/components/assistant/AssistantPanel' import DateDependencyMenuItem from '@baserow_enterprise/components/dateDependency/DateDependencyMenuItem' import DateDependencyFieldTypeIcon from '@baserow_enterprise/components/dateDependency/DateDependencyFieldTypeIcon' -import { FF_ASSISTANT } from '@baserow/modules/core/plugins/featureFlags' export class EnterprisePlugin extends BaserowPlugin { static getType() { @@ -23,9 +22,7 @@ export class EnterprisePlugin extends BaserowPlugin { getSidebarWorkspaceComponents(workspace) { const sidebarItems = [] - if (this.app.$featureFlagIsEnabled(FF_ASSISTANT)) { - sidebarItems.push(AssistantSidebarItem) - } + sidebarItems.push(AssistantSidebarItem) if (!this.app.$config.BASEROW_DISABLE_SUPPORT) { sidebarItems.push(ChatwootSupportSidebarWorkspace) } @@ -51,9 +48,7 @@ export class EnterprisePlugin extends BaserowPlugin { getRightSidebarWorkspaceComponents(workspace) { const rightSidebarItems = [] - if (this.app.$featureFlagIsEnabled(FF_ASSISTANT)) { - rightSidebarItems.push(AssistantPanel) - } + rightSidebarItems.push(AssistantPanel) return rightSidebarItems } diff --git a/web-frontend/modules/core/plugins/featureFlags.js b/web-frontend/modules/core/plugins/featureFlags.js index e0534677d5..b44a38ce27 100644 --- a/web-frontend/modules/core/plugins/featureFlags.js +++ b/web-frontend/modules/core/plugins/featureFlags.js @@ -1,5 +1,4 @@ const FF_ENABLE_ALL = '*' -export const FF_ASSISTANT = 'assistant' export const FF_WORKSPACE_SEARCH = 'workspace-search' export const FF_DATE_DEPENDENCY = 'date_dependency' From 4a41d1a14e2285f775337ab0ad2b21c13317d9c1 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 17 Nov 2025 19:24:56 +0100 Subject: [PATCH 5/6] Translation for 2.0 (#4276) --- .../baserow_enterprise/locales/de.json | 3 + .../baserow_enterprise/locales/fr.json | 55 ++++++- .../baserow_enterprise/locales/ko.json | 52 ++++++- .../baserow_enterprise/locales/nl.json | 50 +++++- .../modules/baserow_premium/locales/de.json | 19 ++- .../modules/baserow_premium/locales/fr.json | 24 ++- .../modules/baserow_premium/locales/ko.json | 24 ++- .../modules/baserow_premium/locales/nl.json | 22 ++- web-frontend/locales/de.json | 26 +++- web-frontend/locales/fr.json | 64 +++++++- web-frontend/locales/ko.json | 64 +++++++- web-frontend/locales/nl.json | 64 +++++++- web-frontend/modules/builder/locales/de.json | 54 ++++++- web-frontend/modules/builder/locales/fr.json | 8 +- web-frontend/modules/builder/locales/ko.json | 8 +- web-frontend/modules/builder/locales/nl.json | 8 +- web-frontend/modules/core/locales/de.json | 13 +- web-frontend/modules/core/locales/fr.json | 142 ++++++++++++++++- web-frontend/modules/core/locales/ko.json | 142 ++++++++++++++++- web-frontend/modules/core/locales/nl.json | 140 ++++++++++++++++- web-frontend/modules/database/locales/fr.json | 11 +- web-frontend/modules/database/locales/ko.json | 6 +- web-frontend/modules/database/locales/nl.json | 9 +- .../modules/integrations/locales/de.json | 144 +++++++++++++++++- .../modules/integrations/locales/fr.json | 103 ++++++++++++- .../modules/integrations/locales/ko.json | 107 +++++++++++-- .../modules/integrations/locales/nl.json | 97 +++++++++++- 27 files changed, 1371 insertions(+), 88 deletions(-) diff --git a/enterprise/web-frontend/modules/baserow_enterprise/locales/de.json b/enterprise/web-frontend/modules/baserow_enterprise/locales/de.json index 04eed141ab..ccef568049 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/locales/de.json +++ b/enterprise/web-frontend/modules/baserow_enterprise/locales/de.json @@ -403,5 +403,8 @@ }, "oidcAuthLink": { "placeholderWithOIDC": "{login} mit {provider}" + }, + "advanced": { + "license": "Advanced" } } diff --git a/enterprise/web-frontend/modules/baserow_enterprise/locales/fr.json b/enterprise/web-frontend/modules/baserow_enterprise/locales/fr.json index bbc6b6817e..5d59bdee19 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/locales/fr.json +++ b/enterprise/web-frontend/modules/baserow_enterprise/locales/fr.json @@ -626,7 +626,7 @@ "realtimePushDescription": "Permet de modifier les valeurs des cellules directement dans Baserow et de les envoyer en temps réel à la source de synchronisation des données. Notez que les modifications ne sont pas récupérées en temps réel, car elles dépendent de l'action de synchronisation (périodique) pour être effectuées. Il est fortement recommandé de créer des sauvegardes de la base de données PostgreSQL afin d'éviter toute modification accidentelle." }, "assistantSidebarItem": { - "title": "Assistant IA" + "title": "Kuma IA" }, "assistantPanel": { "title": "Assistant IA", @@ -636,14 +636,30 @@ }, "assistantWelcomeMessage": { "greet": "Bonjour {name}", - "question": "comment puis-je aider ?", - "subtitle": "Je suis là pour vous aider à gérer vos données." + "question": "mon nom est Kuma !", + "subtitle": "Je suis là pour vous aider à utiliser Baserow. Choisissez parmi les prompts ci-dessous ou écrivez-moi simplement ce dont vous avez besoin !", + "promptInviteUsersPrompt": "Comment inviter les utilisateurs dans mon projet ?", + "promptCreateTableTitle": "Créer une table", + "promptCreateTablePrompt": "Créer une nouvelle table nommée tâches.", + "promptWhichTablesTitle": "Quelles tables", + "promptWhichTablesPrompt": "Quelles tables sont dans cette base de données ?", + "promptCreateFormTitle": "Créer un formulaire", + "promptCreateFormPrompt": "Créer un formulaire pour cette table.", + "promptCreateFilterTitle": "Créer un filtre", + "promptCreateFilterPrompt": "Montrer seulement les lignes où le champ primaire est vide.", + "subtitleWithoutSuggestions": "Je suis là pour vous faciliter l'utilisation de Baserow. Dites-moi ce dont vous avez besoin !", + "promptCreateDatabaseTitle": "Créer une base de données", + "promptCreateDatabasePrompt": "Construire une base de données de gestion de projet.", + "promptCreateAutomationTitle": "Créer une automatisation", + "promptCreateAutomationPrompt": "Créez une automatisation pour que chaque mardi matin une demande soit envoyée dans le canal Slack des développeurs pour savoir s'il y a une nouvelle fonctionnalité à présenter.", + "promptInviteUsersTitle": "Inviter des utilisateurs" }, "assistantInputMessage": { "statusWaiting": "L'assistant est prêt à vous aider", "statusRunning": "En cours...", "placeholder": "Demandez-moi ce que vous voulez...", - "send": "Envoyer" + "send": "Envoyer", + "stop": "Arréter la génération" }, "assistantChatHistoryContext": { "empty": "Aucun historique de conversation", @@ -670,5 +686,36 @@ "includeWeekendsLabel": "Inclure les week-ends dans le calcul des durées", "dependencyFieldForReaderTooltip": "Ce champ est inclus dans la règle de dépendance de date", "fieldInvalidTitle": "Erreur dans la dépendance de date" + }, + "assistant": { + "statusThinking": "Réflexions...", + "statusAnswering": "Réponse en cours...", + "statusCancelling": "Annulation...", + "messageCancelled": "Message annulé" + }, + "assistantMessageSources": { + "sources": "{count} source | {count} sources" + }, + "assistantMessageActions": { + "feedbackContextTitle": "Aidez-nous à nous améliorer", + "feedbackContextPlaceholder": "Que pourrions-nous améliorer ? (facultatif)", + "copiedToClipboard": "Copié dans le presse-papiers", + "copiedContentToast": "La réponse de l'assistant a été copié dans votre presse-papiers", + "copyFailed": "Échec de la copie dans le presse-papiers", + "disclaimer": "Kuma peut faire des erreurs, veuillez vérifier les réponses" + }, + "dateDependency": { + "invalidChildRow": "La ligne suivante est invalide", + "invalidParentRow": "La ligne précédente est invalide", + "invalidParentEndDateAfterChildStartDate": "La date de fin de la ligne précédente est postérieure à la date de début de la ligne suivante", + "invalidStartDateEmpty": "La date de début est vide", + "invalidEndDateEmpty": "La date de fin est vide", + "invalidEndDateBeforeStartDate": "La date de fin est antérieure à la date de début", + "invalidDurationEmpty": "La durée est vide", + "invalidDurationValue": "La durée n'est pas valide", + "invalidDurationMismatch": "La durée ne correspond pas" + }, + "assistantMessageList": { + "disclaimer": "Kuma peut faire des erreurs, veuillez vérifier les réponses" } } diff --git a/enterprise/web-frontend/modules/baserow_enterprise/locales/ko.json b/enterprise/web-frontend/modules/baserow_enterprise/locales/ko.json index 3481055fa4..9585048e43 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/locales/ko.json +++ b/enterprise/web-frontend/modules/baserow_enterprise/locales/ko.json @@ -623,7 +623,7 @@ "realtimePushDescription": "Baserow에서 셀 값을 직접 편집하고 실시간으로 데이터 동기화 소스에 푸시합니다. 변경 사항은 (주기적인) 동기화 작업에 의존하기 때문에 실시간으로 가져오지 않습니다. 실수로 변경되는 것을 방지하려면 PostgreSQL 데이터베이스 백업을 생성하는 것이 좋습니다." }, "assistantSidebarItem": { - "title": "AI 어시스턴트" + "title": "Kuma AI" }, "assistantPanel": { "title": "AI 어시스턴트", @@ -633,14 +633,30 @@ }, "assistantWelcomeMessage": { "greet": "안녕 {name}", - "question": "어떻게 도와드릴까요?", - "subtitle": "저는 귀하의 데이터 처리를 지원하기 위해 여기에 있습니다." + "question": "제 이름은 쿠마예요!", + "subtitle": "짐이 무겁게 느껴지지 않도록 제가 도와드릴게요. 아래 질문 중에서 선택하시거나, 필요하신 내용을 말씀해 주세요!", + "subtitleWithoutSuggestions": "일이 부담스럽게 느껴지지 않도록 제가 도와드릴게요. 필요한 게 뭐든지 말씀만 하세요!", + "promptCreateDatabaseTitle": "데이터베이스 생성", + "promptCreateDatabasePrompt": "프로젝트 관리 데이터베이스를 구축합니다.", + "promptCreateAutomationTitle": "자동화 만들기", + "promptCreateAutomationPrompt": "매주 화요일 아침 Slack 개발자 채널에서 데모할 내용이 있는지 묻는 자동화를 만듭니다.", + "promptInviteUsersTitle": "사용자 초대", + "promptInviteUsersPrompt": "내 작업 공간에 사용자를 초대하려면 어떻게 해야 하나요?", + "promptCreateTableTitle": "테이블 생성", + "promptCreateTablePrompt": "작업이라는 이름의 새 테이블을 만듭니다.", + "promptWhichTablesTitle": "어떤 테이블", + "promptWhichTablesPrompt": "이 데이터베이스에는 어떤 테이블이 있나요?", + "promptCreateFormTitle": "양식 생성", + "promptCreateFormPrompt": "이 표에 대한 양식을 만듭니다.", + "promptCreateFilterTitle": "필터 만들기", + "promptCreateFilterPrompt": "기본 필드가 비어 있는 행만 표시합니다." }, "assistantInputMessage": { "statusWaiting": "어시스턴트가 도와드릴 준비가 되었습니다", "statusRunning": "실행중...", "placeholder": "무엇이든 물어보세요...", - "send": "메시지 보내기" + "send": "메시지 보내기", + "stop": "생성 중지" }, "assistantChatHistoryContext": { "empty": "채팅 기록이 없습니다", @@ -667,5 +683,33 @@ "includeWeekendsLabel": "기간을 계산할 때 주말을 포함", "dependencyFieldForReaderTooltip": "이 필드는 날짜 종속성 필드 규칙에 포함됩니다", "fieldInvalidTitle": "날짜 종속성 필드 오류" + }, + "assistant": { + "statusThinking": "생각중...", + "statusAnswering": "답변중...", + "statusCancelling": "취소중...", + "messageCancelled": "메시지 취소됨" + }, + "assistantMessageSources": { + "sources": "{count} 소스 | {count} 소스" + }, + "assistantMessageActions": { + "feedbackContextTitle": "개선에 도움을 주세요", + "feedbackContextPlaceholder": "무엇을 개선할 수 있을까요? (선택 사항)", + "copiedToClipboard": "클립보드에 복사됨", + "copiedContentToast": "도우미의 응답 내용이 클립보드에 복사되었습니다", + "copyFailed": "클립보드에 복사하는 데 실패했습니다", + "disclaimer": "쿠마는 실수를 할 수 있으니 답변을 다시 한번 확인해 주세요" + }, + "dateDependency": { + "invalidChildRow": "후속 행이 유효하지 않습니다", + "invalidParentRow": "선행 행이 잘못되었습니다", + "invalidParentEndDateAfterChildStartDate": "이전 행 종료 날짜가 후속 행 시작 날짜 이후입니다", + "invalidStartDateEmpty": "시작 날짜가 비어 있습니다", + "invalidEndDateEmpty": "종료 날짜가 비어 있습니다", + "invalidEndDateBeforeStartDate": "종료 날짜가 시작 날짜보다 이전입니다", + "invalidDurationEmpty": "기간이 비어 있습니다", + "invalidDurationValue": "기간 값이 유효하지 않습니다", + "invalidDurationMismatch": "기간 값 불일치" } } diff --git a/enterprise/web-frontend/modules/baserow_enterprise/locales/nl.json b/enterprise/web-frontend/modules/baserow_enterprise/locales/nl.json index 61face9465..b9c624dd48 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/locales/nl.json +++ b/enterprise/web-frontend/modules/baserow_enterprise/locales/nl.json @@ -637,13 +637,29 @@ "assistantWelcomeMessage": { "greet": "Hé {name}", "question": "hoe kan ik helpen?", - "subtitle": "Ik ben er om je te ondersteunen bij het omgaan met je gegevens." + "subtitle": "Ik ben er om je te ondersteunen bij het omgaan met je gegevens.", + "subtitleWithoutSuggestions": "Ik help je de last te dragen zodat het nooit zwaar aanvoelt. Vertel me gewoon wat je nodig hebt!", + "promptCreateDatabaseTitle": "Een database maken", + "promptCreateDatabasePrompt": "Bouw een database voor projectbeheer.", + "promptCreateAutomationTitle": "Een automatisering maken", + "promptCreateAutomationPrompt": "Maak een automatisering die elke dinsdag in de ochtend in het Slack developers kanaal vraagt of er iets te demonstreren valt.", + "promptInviteUsersTitle": "Gebruikers uitnodigen", + "promptInviteUsersPrompt": "Hoe nodig ik gebruikers uit voor mijn werkruimte?", + "promptCreateTableTitle": "Een tabel maken", + "promptCreateTablePrompt": "Maak een nieuwe tabel met de naam taken.", + "promptWhichTablesTitle": "Welke tabellen", + "promptWhichTablesPrompt": "Welke tabellen heb ik in deze database?", + "promptCreateFormTitle": "Een formulier maken", + "promptCreateFormPrompt": "Maak een formulier voor deze tabel.", + "promptCreateFilterTitle": "Een filter maken", + "promptCreateFilterPrompt": "Toon alleen rijen waarvan het primaire veld leeg is." }, "assistantInputMessage": { "statusWaiting": "Assistent staat klaar om te helpen", "statusRunning": "Uitvoeren...", "placeholder": "Vraag me alles...", - "send": "Verstuur bericht" + "send": "Verstuur bericht", + "stop": "Genereren stoppen" }, "assistantChatHistoryContext": { "empty": "Geen chatgeschiedenis beschikbaar", @@ -670,5 +686,35 @@ "includeWeekendsLabel": "Houd rekening met weekenden bij het berekenen van looptijden", "dependencyFieldForReaderTooltip": "Dit veld is opgenomen in de regel voor datumafhankelijke velden", "fieldInvalidTitle": "Fout in datumafhankelijkheidsveld" + }, + "assistant": { + "statusThinking": "Denken...", + "statusAnswering": "Antwoorden...", + "statusCancelling": "Annuleren...", + "messageCancelled": "Bericht geannuleerd" + }, + "assistantMessageSources": { + "sources": "{count} source | {count} sources" + }, + "assistantMessageActions": { + "feedbackContextTitle": "Help ons te verbeteren", + "feedbackContextPlaceholder": "Wat kunnen we verbeteren? (optioneel)", + "copiedToClipboard": "Gekopieerd naar klembord", + "copiedContentToast": "De inhoud van het antwoord van de assistent is gekopieerd naar je klembord", + "copyFailed": "Kopiëren naar klembord mislukt" + }, + "dateDependency": { + "invalidChildRow": "Opeenvolgende rij is ongeldig", + "invalidParentRow": "Voorganger rij is ongeldig", + "invalidParentEndDateAfterChildStartDate": "De einddatum van de rij van de voorganger ligt na de begindatum van de rij van de opvolger", + "invalidStartDateEmpty": "Startdatum is leeg", + "invalidEndDateEmpty": "Einddatum is leeg", + "invalidEndDateBeforeStartDate": "Einddatum is voor begindatum", + "invalidDurationEmpty": "Duur is leeg", + "invalidDurationValue": "Duur waarde is niet geldig", + "invalidDurationMismatch": "Duur waarde mismatch" + }, + "assistantMessageList": { + "disclaimer": "Kuma kan fouten maken, controleer antwoorden dubbel" } } diff --git a/premium/web-frontend/modules/baserow_premium/locales/de.json b/premium/web-frontend/modules/baserow_premium/locales/de.json index 2c43a6161f..bba40289ba 100644 --- a/premium/web-frontend/modules/baserow_premium/locales/de.json +++ b/premium/web-frontend/modules/baserow_premium/locales/de.json @@ -110,7 +110,8 @@ "expired": "Abgelaufen", "validity": "Gültig von {start} bis {end}", "seats": "Sitze", - "premiumFeatures": "Premium-Funktionen" + "premiumFeatures": "Premium-Funktionen", + "applicationUsers": "Anwendungsbenutzer" }, "license": { "title": "{name} Plan", @@ -193,7 +194,21 @@ "personalViews": "Persönliche Ansichten", "calendarView": "Kalenderansicht", "aiFeatures": "KI-Feld", - "timelineView": "Zeitleisten-Ansicht" + "timelineView": "Zeitleisten-Ansicht", + "rowCommentsContent": "Arbeiten Sie direkt in Ihrer Tabelle zusammen. Fügen Sie Kommentare zu einzelnen Zeilen hinzu, erwähnen Sie Teammitglieder und behalten Sie den Kontext der Unterhaltung im Blick.", + "kanbanViewContent": "Organisieren Sie Ihre Daten visuell auf einem Kanban-Board. Karten werden anhand eines einzigen Auswahlfeldes in Stapeln gruppiert und Sie können sie ganz einfach per Drag & Drop zwischen den Stapeln verschieben.", + "calendarViewContent": "Zeigen Sie Ihre Zeilen ganz einfach in einem Kalender an, indem Sie ein Datumsfeld Ihrer Wahl verwenden. Perfekt, um Termine, Ereignisse und Zeitpläne gesammelt im Blick zu behalten.", + "timelineViewContent": "Zeigen Sie Zeilen entlang einer horizontalen Zeitachse anhand von Start- und Enddaten an. Ideal für die Projektplanung und Roadmaps.", + "exportsContent": "Exportieren Sie Ihre Daten ganz einfach in weitere Formate wie Excel, JSON und XML oder laden Sie alle Dateien aus einer Tabelle als Massendownload herunter – ideal für Backups, Berichte und die Weitergabe an andere.", + "rowColoringContent": "Markieren Sie Zeilen mit Farben basierend auf einem einzelnen Auswahlfeld oder benutzerdefinierten Bedingungen. Dadurch lassen sich Muster und Prioritäten leichter erkennen.", + "rowNotifications": "Zeilenbenachrichtigungen", + "rowNotificationsContent": "Bleiben Sie mit anpassbaren Zeilenkommentar-Benachrichtigungen auf dem Laufenden. Wählen Sie zwischen Benachrichtigungen für alle Kommentare oder nur für Kommentare, in denen Sie erwähnt werden.", + "surveyFormContent": "Sammeln Sie Frage für Frage Antworten im Umfragemodus. Eine vereinfachte Version der Formularansicht, optimiert für Fokus und Benutzerfreundlichkeit.", + "publicLogoRemovalContent": "Entfernen Sie das Baserow-Logo aus öffentlichen Ansichten, um Ihren geteilten Inhalten ein klareres, markengerechteres Erscheinungsbild zu verleihen.", + "personalViewsContent": "Erstellen Sie Ansichten, die nur der Autor sehen kann. So haben Sie Raum zum Experimentieren und Filtern und Sortieren von Daten, ohne den Arbeitsbereich anderer zu beeinträchtigen.", + "aiFeaturesContent": "Nutzen Sie KI, um Ihre Arbeitsabläufe zu automatisieren und zu verbessern. Mit dem KI-Eingabefeldtyp können Sie Texte, Zusammenfassungen oder Erkenntnisse direkt in Zellen generieren, basierend auf dynamischen Eingabeaufforderungen, die auf andere Felder verweisen können. Unabhängig davon können Sie die KI auch bitten, Formeln für Sie zu erstellen.", + "chartWidget": "Diagramm-Widget", + "chartWidgetContent": "Visualisieren Sie Ihre Daten mit Diagrammen auf Dashboards. Verwenden Sie beliebige Tabellen als Datenquelle, fügen Sie mehrere Reihen hinzu und gruppieren oder sortieren Sie nach Feldern mit umfangreichen Zusammenfassungsoptionen." }, "premiumModal": { "description": "Ihr Konto hat keinen Zugang zu den Premium-Funktionen. Führen Sie ein Upgrade auf die Premium-Version durch, um {name} nutzen zu können. Sie können Ihr Konto erweitern, indem Sie eine Lizenz erwerben. Klicken Sie auf den Knopf unten, um die Preise zu sehen.", diff --git a/premium/web-frontend/modules/baserow_premium/locales/fr.json b/premium/web-frontend/modules/baserow_premium/locales/fr.json index 46388bb447..87509aa7ce 100644 --- a/premium/web-frontend/modules/baserow_premium/locales/fr.json +++ b/premium/web-frontend/modules/baserow_premium/locales/fr.json @@ -299,7 +299,10 @@ "promptPlaceholder": "Qu'est-ce que Baserow ?", "outputTypeChangedWarning": "Efface les valeurs des cellules générées.", "outputTypeTooltip": "Sélectionnez le format de sortie souhaité pour aider le LLM à générer des réponses correspondant aux options désirées.", - "outputType": "Format du résultat" + "outputType": "Format du résultat", + "autoUpdate": "Mise à jour automatique", + "autoUpdateHelp": "Lorsque cette option est activée, la valeur du champ IA est automatiquement régénérée lorsque l'un des champs référencés dans l'invite est mis à jour.", + "autoUpdateDescription": "Régénérer lorsque les champs référencés changent" }, "rowEditFieldAI": { "createRowBefore": "La valeur du champ IA peut être générée après la création de la ligne.", @@ -311,7 +314,7 @@ "aiFormulaModal": { "title": "Générer une formule à l'aide de l'IA", "description": "Notez que les formules générées ne fonctionnent pas toujours comme prévu. Nous construisons un prompt pour le modèle et nous utilisons le résultat en tant que formule. Cela fonctionne mieux avec un modèle puissant comme le modèle *gpt-4-turbo-preview*.", - "noModels": "Aucun modèle d'IA n'est configuré dans votre instance et votre projet Baserow. Cliquez sur les trois points à côté de votre espace de travail, puis sur les paramètres pour en configurer.", + "noModels": "Aucun modèle d'IA n'est configuré dans votre instance et votre projet Baserow. Naviguez vers la page accueil, cliquez sur le nom de votre projet, puis sur les paramètres pour en configurer un.", "label": "Prompt", "labelDescription": "Décrivez la formule que vous souhaitez générer", "generate": "Générer" @@ -388,5 +391,22 @@ "other": "Autre", "true": "vrai", "false": "faux" + }, + "generateAIValuesModal": { + "title": "Générer toutes les valeurs IA" + }, + "generateAIValuesForm": { + "scopeLabel": "Champ d ' application", + "entireTable": "Table entière", + "skipPopulated": "Générer uniquement des valeurs pour les cellules vides", + "warningTitle": "Avertissement d'utilisation du Jeton", + "warningMessage": "Générer toutes les valeurs consomme des jetons d'API et peut prendre un temps considérable en fonction du nombre de lignes." + }, + "generateAIValuesFormFooter": { + "generate": "Générer avec l'IA", + "close": "Fermer" + }, + "jobType": { + "generateAIValues": "Régénérer les valeurs du champ" } } diff --git a/premium/web-frontend/modules/baserow_premium/locales/ko.json b/premium/web-frontend/modules/baserow_premium/locales/ko.json index 578740e0d4..7199d94864 100644 --- a/premium/web-frontend/modules/baserow_premium/locales/ko.json +++ b/premium/web-frontend/modules/baserow_premium/locales/ko.json @@ -332,7 +332,10 @@ "fileFieldHelp": "필드의 첫 번째 호환 가능한 파일이 프롬프트의 지식 기반으로 사용됩니다. 파일은 .txt, .md, .pdf, .docx와 같은 지원되는 파일 확장자를 가진 텍스트 파일이어야 합니다.", "outputType": "출력 유형", "outputTypeTooltip": "원하는 출력 형식을 선택하여 LLM이 원하는 옵션에 맞는 응답을 생성하도록 안내하세요.", - "outputTypeChangedWarning": "생성된 셀 값을 지웁니다." + "outputTypeChangedWarning": "생성된 셀 값을 지웁니다.", + "autoUpdate": "자동 업데이트", + "autoUpdateHelp": "이 기능을 활성화하면 프롬프트에서 참조된 필드가 업데이트될 때 AI 필드 값이 자동으로 다시 생성됩니다.", + "autoUpdateDescription": "참조된 필드가 변경될 때 재생성" }, "rowEditFieldAI": { "generate": "생성", @@ -344,7 +347,7 @@ "label": "프롬프트", "labelDescription": "생성하려는 수식을 설명하세요", "generate": "생성", - "noModels": "귀하의 Baserow 인스턴스와 작업공간에 구성된 AI 모델이 없습니다. 작업공간 옆의 세 점을 클릭한 다음 설정을 클릭하여 구성하세요." + "noModels": "Baserow 인스턴스와 작업 공간에 AI 모델이 구성되어 있지 않습니다. 홈페이지로 이동하여 작업 공간 이름을 클릭한 다음 설정을 클릭하여 구성하세요." }, "formulaFieldAI": { "generateWithAI": "AI를 사용하여 생성", @@ -376,5 +379,22 @@ "other": "기타", "true": "참", "false": "거짓" + }, + "generateAIValuesModal": { + "title": "모든 AI 값 생성" + }, + "generateAIValuesForm": { + "scopeLabel": "범위", + "entireTable": "전체 테이블", + "skipPopulated": "빈 셀에 대한 값만 생성합니다", + "warningTitle": "토큰 사용 경고", + "warningMessage": "모든 AI 값을 생성하려면 API 토큰이 소모되며 행 수에 따라 상당한 시간이 걸릴 수 있습니다." + }, + "generateAIValuesFormFooter": { + "generate": "AI로 생성", + "close": "닫기" + }, + "jobType": { + "generateAIValues": "AI 필드 값 재생성" } } diff --git a/premium/web-frontend/modules/baserow_premium/locales/nl.json b/premium/web-frontend/modules/baserow_premium/locales/nl.json index 47420f4447..1c9d413653 100644 --- a/premium/web-frontend/modules/baserow_premium/locales/nl.json +++ b/premium/web-frontend/modules/baserow_premium/locales/nl.json @@ -298,7 +298,10 @@ "promptPlaceholder": "Wat is Baserow?", "outputType": "Uitkomst type", "outputTypeTooltip": "Selecteer de gewenste uitkomst format om de LLM te begeleiden bij het genereren van reacties die overeenkomen met de gewenste opties.", - "outputTypeChangedWarning": "Wist de gegenereerde celwaarden." + "outputTypeChangedWarning": "Wist de gegenereerde celwaarden.", + "autoUpdate": "Automatisch bijwerken", + "autoUpdateHelp": "Als deze optie is ingeschakeld, wordt de waarde van het AI-veld automatisch opnieuw gegenereerd als een van de velden waarnaar wordt verwezen als de prompt wordt bijgewerkt.", + "autoUpdateDescription": "Opnieuw genereren wanneer velden waarnaar wordt verwezen veranderen" }, "rowEditFieldAI": { "generate": "Genereren", @@ -388,5 +391,22 @@ "other": "Andere", "true": "waar", "false": "onwaar" + }, + "generateAIValuesModal": { + "title": "Genereer alle AI-waarden" + }, + "generateAIValuesForm": { + "scopeLabel": "Scope", + "entireTable": "Gehele tabel", + "skipPopulated": "Genereer alleen waarden voor lege cellen", + "warningTitle": "Waarschuwing voor tokengebruik", + "warningMessage": "Het genereren van alle AI waarden verbruikt API-tokens en kan veel tijd in beslag nemen, afhankelijk van het aantal rijen." + }, + "generateAIValuesFormFooter": { + "generate": "Genereren met AI", + "close": "Sluit" + }, + "jobType": { + "generateAIValues": "AI-veldwaarden opnieuw genereren" } } diff --git a/web-frontend/locales/de.json b/web-frontend/locales/de.json index 3afffac48f..440d37392b 100644 --- a/web-frontend/locales/de.json +++ b/web-frontend/locales/de.json @@ -13,7 +13,16 @@ "summarize": "Zusammenfassen", "disabled": "Deaktiviert", "or": "oder", - "and": "und" + "and": "und", + "alpha": "Alpha", + "optional": "Optional", + "monday": "Montag", + "tuesday": "Dienstag", + "wednesday": "Mittwoch", + "thursday": "Donnerstag", + "friday": "Freitag", + "saturday": "Samstag", + "sunday": "Sonntag" }, "action": { "upload": "Hochladen", @@ -75,14 +84,16 @@ "automationDefaultName": "Unbenannte Automatisierung", "cantSelectAutomationWorkflowTitle": "Konnte die Automatisierung nicht auswählen.", "automation": "Automatisierung", - "automations": "Automatisierungen" + "automations": "Automatisierungen", + "cantSelectAutomationWorkflowDescription": "Die Automatisierung konnte nicht ausgewählt werden, da sie keine Workflows enthält. Verwenden Sie die Seitenleiste, um einen zu erstellen." }, "settingType": { "account": "Konto", "password": "Passwort", "tokens": "Datenbank-Token", "deleteAccount": "Konto löschen", - "emailNotifications": "E-Mailbenachrichtigungen" + "emailNotifications": "E-Mailbenachrichtigungen", + "mcpEndpoint": "MCP-Server" }, "userFileUploadType": { "file": "Mein Gerät", @@ -107,7 +118,8 @@ "invalidURL": "Bitte geben Sie eine gültige URL ein.", "alreadyInUse": "Dieser Feldname wird bereits verwendet.", "invalidCharacters": "Das Feld beinhaltet ungültige Zeichen.", - "decimalField": "Das Feld muss eine Dezimalzahl sein." + "decimalField": "Das Feld muss eine Dezimalzahl sein.", + "copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen. Bitte versuchen Sie es erneut." }, "permission": { "admin": "Administration", @@ -147,9 +159,11 @@ "duration": "Dauer", "multipleSelectCheckboxes": "Kontrollkästchen", "autonumber": "Automatische Nummerierung", - "singleSelectDropdown": "Dropdown", + "singleSelectDropdown": "Dropdown-Menü", "password": "Passwort", - "ai": "KI-Eingabeaufforderung" + "ai": "KI-Eingabeaufforderung", + "multipleCollaboratorsDropdown": "Dropdown-Menü", + "multipleCollaboratorsCheckboxes": "Kontrollkästchen" }, "fieldErrors": { "invalidNumber": "Ungültige Zahl", diff --git a/web-frontend/locales/fr.json b/web-frontend/locales/fr.json index 3b9d5fb422..77e5e0161d 100644 --- a/web-frontend/locales/fr.json +++ b/web-frontend/locales/fr.json @@ -24,7 +24,8 @@ "thursday": "Jeudi", "friday": "Vendredi", "saturday": "Samedi", - "sunday": "Dimanche" + "sunday": "Dimanche", + "value": "valeur" }, "action": { "upload": "Envoyer", @@ -95,7 +96,8 @@ "tokens": "Jetons d'accès à la base", "deleteAccount": "Supprimer le compte", "emailNotifications": "Notifications par courriel", - "mcpEndpoint": "Serveur MCP" + "mcpEndpoint": "Serveur MCP", + "twoFactorAuth": "Autorisation à deux facteurs" }, "userFileUploadType": { "file": "de mon appareil", @@ -642,5 +644,63 @@ }, "fieldConstraintParametersText": { "placeholder": "Saisir la valeur de la contrainte" + }, + "assistantMessageSources": { + "sources": "{count} source | {count} sources" + }, + "runtimeFormulaTypes": { + "concatDescription": "Regroupe les arguments en une seule chaîne de charactères.", + "getDescription": "Retourne la valeur de la donnée spécifée.", + "addDescription": "Additionne deux arguments.", + "minusDescription": "Soustrait le deuxième argument du premier.", + "multiplyDescription": "Multiplie les deux arguments.", + "divideDescription": "Divise le premier argument par le second.", + "equalDescription": "Vérifie si les deux arguments sont égaux.", + "notEqualDescription": "Vérifie si les deux arguments sont différents.", + "greaterThanDescription": "Vérifie si le premier argument est supérieur au second.", + "lessThanDescription": "Vérifie si le premier argument est inférieur au second.", + "greaterThanOrEqualDescription": "Vérifie si le premier argument est supérieur ou égal au second.", + "upperDescription": "Tranforme la chaîne en majuscules.", + "lowerDescription": "Transforme la chaîne en minuscule.", + "capitalizeDescription": "Met la première lettre de l'argument en majuscule.", + "roundDescription": "Arrondit le premier argument au nombre de décimales spécifié par le second argument.", + "evenDescription": "Retourne true si l'argument est pair, false sinon.", + "oddDescription": "Retourne true si l'argument est impair, false sinon.", + "dateTimeDescription": "Formate la date en argument en utilisant les arguments format et timezone. Si le fuseau horaire n'est pas renseigné, le fuseau horaire du navigateur sera utilisé par défaut.", + "dayDescription": "Renvoie le jour de la date fournie en argument.", + "monthDescription": "Renvoie le mois de la date fournie en argument.", + "yearDescription": "Renvoie l'année de la date fournie en argument.", + "hourDescription": "Renvoie l'heure de la date fournie en argument.", + "minuteDescription": "Renvoie les minutes de la date fournie en argument.", + "secondDescription": "Renvoie les secondes de la date fournie en argument.", + "todayDescription": "Retourne la date courante.", + "getPropertyDescription": "Renvoie la propriété de l'objet.", + "randomIntDescription": "Renvoie un nombre entier aléatoire dans l'intervalle spécifié par les arguments.", + "randomFloatDescription": "Renvoie un flottant aléatoire dans l'intervalle spécifié par les arguments.", + "randomBoolDescription": "Renvoie un booléen aléatoire.", + "generateUUIDDescription": "Renvoie une chaîne UUID4 aléatoire.", + "ifDescription": "Si le premier argument est vrai, renvoie le deuxième argument, sinon renvoie le troisième argument.", + "andDescription": "Retourne vrai si tous les arguments sont vrais, sinon retourne faux.", + "orDescription": "Retourne vrai si l'un des arguments est vrai, sinon retourne faux.", + "formulaTypeFormula": "Fonction | Fonctions", + "formulaTypeOperator": "Opérateur | Opérateurs", + "formulaTypeData": "Données", + "formulaTypeDataEmpty": "Aucune donnée disponible", + "categoryText": "Texte", + "categoryNumber": "Nombre", + "categoryBoolean": "Booléen", + "categoryDate": "Date", + "categoryCondition": "Condition" + }, + "formulaInputContext": { + "useRegularInput": "Utiliser une formule simple", + "useAdvancedInput": "Utiliser la saisie avancée", + "useRegularInputModalTitle": "Passer à une formule simple ?", + "useAdvancedInputModalTitle": "Passer à la formule avancée ?", + "modalMessage": "Le passage à un autre mode effacera la formule actuelle. Êtes-vous sûr‐e de vouloir continuer ?" + }, + "nodeExplorer": { + "noResults": "Aucun résultat trouvé", + "resetSearch": "Réinitialiser la recherche" } } diff --git a/web-frontend/locales/ko.json b/web-frontend/locales/ko.json index e943807573..e13b7202ae 100644 --- a/web-frontend/locales/ko.json +++ b/web-frontend/locales/ko.json @@ -22,7 +22,8 @@ "thursday": "목요일", "friday": "금요일", "saturday": "토요일", - "sunday": "일요일" + "sunday": "일요일", + "value": "값" }, "action": { "close": "닫기", @@ -93,7 +94,8 @@ "emailNotifications": "이메일 알림", "tokens": "데이터베이스 토큰", "deleteAccount": "계정 삭제", - "mcpEndpoint": "MCP 서버" + "mcpEndpoint": "MCP 서버", + "twoFactorAuth": "2단계 인증" }, "userFileUploadType": { "file": "내 기기", @@ -635,5 +637,63 @@ }, "fieldConstraintParametersText": { "placeholder": "제약 조건 값을 입력하세요" + }, + "assistantMessageSources": { + "sources": "{count} 소스 | {count} 소스" + }, + "runtimeFormulaTypes": { + "concatDescription": "인수들을 하나의 텍스트로 결합했습니다.", + "getDescription": "Baserow 수식의 계산된 값을 반환합니다.", + "addDescription": "두 인수를 서로 더합니다.", + "minusDescription": "첫 번째 인수에서 두 번째 인수를 뺍니다.", + "multiplyDescription": "두 인수를 서로 곱합니다.", + "divideDescription": "첫 번째 인수를 두 번째 인수로 나눕니다.", + "equalDescription": "두 인수가 같은지 확인합니다.", + "notEqualDescription": "두 인수가 같지 않은지 확인합니다.", + "greaterThanDescription": "첫 번째 인수가 두 번째 인수보다 큰지 확인합니다.", + "lessThanDescription": "첫 번째 인수가 두 번째 인수보다 작은지 확인합니다.", + "greaterThanOrEqualDescription": "첫 번째 인수가 두 번째 인수보다 크거나 같은지 확인합니다.", + "upperDescription": "인수를 대문자로 변환합니다.", + "lowerDescription": "인수를 소문자로 변환합니다.", + "capitalizeDescription": "인수의 첫 글자를 대문자로 시작합니다.", + "roundDescription": "첫 번째 인수를 두 번째 인수로 지정된 소수 자릿수까지 반올림합니다.", + "evenDescription": "인수가 짝수이면 true를 반환하고, 그렇지 않으면 false를 반환합니다.", + "oddDescription": "인수가 홀수이면 true를 반환하고, 그렇지 않으면 false를 반환합니다.", + "dateTimeDescription": "format 및 timezone 인수를 사용하여 날짜/시간 인수의 형식을 지정합니다. 시간대를 비워 두면 브라우저의 시간대가 기본값으로 사용됩니다.", + "dayDescription": "날짜 시간 인수에서 요일을 반환합니다.", + "monthDescription": "날짜 시간 인수에서 월을 반환합니다.", + "yearDescription": "날짜 시간 인수에서 연도를 반환합니다.", + "hourDescription": "날짜 시간 인수에서 시간을 반환합니다.", + "minuteDescription": "날짜 시간 인수에서 분을 반환합니다.", + "secondDescription": "날짜 시간 인수에서 초를 반환합니다.", + "todayDescription": "현재 날짜를 반환합니다.", + "getPropertyDescription": "객체에서 속성을 반환합니다.", + "randomIntDescription": "인수로 지정된 범위에서 임의의 정수를 반환합니다.", + "randomFloatDescription": "인수로 지정된 범위에서 난수 부동 소수점을 반환합니다.", + "randomBoolDescription": "true 또는 false의 무작위 부울 값을 반환합니다.", + "generateUUIDDescription": "임의의 UUID4 문자열을 반환합니다.", + "ifDescription": "첫 번째 인수가 참이면 두 번째 인수를 반환하고, 그렇지 않으면 세 번째 인수를 반환합니다.", + "andDescription": "모든 인수가 참이면 참을 반환하고, 그렇지 않으면 거짓을 반환합니다.", + "orDescription": "인수가 참이면 참을 반환하고, 그렇지 않으면 거짓을 반환합니다.", + "formulaTypeFormula": "함수 | 함수", + "formulaTypeOperator": "연산자 | 연산자", + "formulaTypeData": "데이터", + "formulaTypeDataEmpty": "사용 가능한 데이터가 없습니다", + "categoryText": "텍스트", + "categoryNumber": "숫자", + "categoryBoolean": "부울", + "categoryDate": "날짜", + "categoryCondition": "상태" + }, + "formulaInputContext": { + "useRegularInput": "일반적인 입력을 사용", + "useAdvancedInput": "고급 입력 사용", + "useRegularInputModalTitle": "일반 입력으로 전환하시겠습니까?", + "useAdvancedInputModalTitle": "고급 입력으로 전환하시겠습니까?", + "modalMessage": "다른 입력 모드로 전환하면 현재 수식이 삭제됩니다. 계속하시겠습니까?" + }, + "nodeExplorer": { + "noResults": "검색 결과가 없습니다", + "resetSearch": "검색 재설정" } } diff --git a/web-frontend/locales/nl.json b/web-frontend/locales/nl.json index e8d6211042..e090253589 100644 --- a/web-frontend/locales/nl.json +++ b/web-frontend/locales/nl.json @@ -24,7 +24,8 @@ "thursday": "Donderdag", "friday": "Vrijdag", "saturday": "Zaterdag", - "sunday": "Zondag" + "sunday": "Zondag", + "value": "waarde" }, "action": { "upload": "Uploaden", @@ -95,7 +96,8 @@ "tokens": "API-tokens", "deleteAccount": "Account verwijderen", "emailNotifications": "E-mail notificaties", - "mcpEndpoint": "MCP server" + "mcpEndpoint": "MCP server", + "twoFactorAuth": "Tweefactorauth" }, "userFileUploadType": { "file": "mijn apparaat", @@ -642,5 +644,63 @@ }, "fieldConstraintParametersText": { "placeholder": "Beperkingswaarde invoeren" + }, + "assistantMessageSources": { + "sources": "{count} source | {count} sources" + }, + "runtimeFormulaTypes": { + "concatDescription": "Voeg argumenten samen als één tekst.", + "getDescription": "Geeft de vaste waarde van een Baserow formule terug.", + "addDescription": "Telt twee argumenten bij elkaar op.", + "minusDescription": "Trekt het tweede argument af van het eerste.", + "multiplyDescription": "Vermenigvuldigt de twee argumenten.", + "divideDescription": "Deelt het eerste argument met de tweede.", + "equalDescription": "Controleert of beide argumenten gelijk zijn.", + "notEqualDescription": "Controleert of beide argumenten niet gelijk zijn.", + "greaterThanDescription": "Controleert of het eerste argument groter is dan het tweede.", + "lessThanDescription": "Controleert of het eerste argument minder is dan het tweede.", + "greaterThanOrEqualDescription": "Controleert of het eerste argument meer of gelijk is aan het tweede.", + "upperDescription": "Converteert het argument naar hoofdletters.", + "lowerDescription": "Converteert het argument naar kleine letters.", + "capitalizeDescription": "Zet de eerste letter van het argument met een hoofdletter.", + "roundDescription": "Rondt het eerste argument af op het aantal decimalen dat wordt opgegeven door het tweede argument.", + "evenDescription": "Geeft true terug als het argument even is, anders false.", + "oddDescription": "Geeft true terug als het argument oneven is, anders false.", + "dateTimeDescription": "Formatteert het datum-tijd argument met het format en tijdzone argument. Als de tijdzone leeg wordt gelaten, wordt standaard de tijdzone van de browser gebruikt.", + "dayDescription": "Geeft als resultaat de dag van het datum-tijdargument.", + "monthDescription": "Geeft als resultaat de maand van het datum-tijdargument.", + "yearDescription": "Geeft het jaar van het datum-tijdargument.", + "hourDescription": "Geeft het uur van het datum-tijdargument.", + "minuteDescription": "Geeft de minuten van het datum-tijdargument.", + "secondDescription": "Geeft de seconde van het datum-tijdargument.", + "todayDescription": "Geeft de huidige datum.", + "getPropertyDescription": "Geeft de eigenschap van het object terug.", + "randomIntDescription": "Geeft een willekeurig geheel getal terug uit het bereik opgegeven door de argumenten.", + "randomFloatDescription": "Geeft een willekeurige float terug uit het bereik opgegeven door de argumenten.", + "randomBoolDescription": "Geeft een willekeurig booleaans waar of onwaar terug.", + "generateUUIDDescription": "Geeft een willekeurige UUID4-tekenreeks terug.", + "ifDescription": "Als het eerste argument waar is, retourneert het tweede argument, anders retourneert het derde argument.", + "andDescription": "Geeft true als alle argumenten waar zijn, anders false.", + "orDescription": "Geeft waar als een van de argumenten waar is, anders onwaar.", + "formulaTypeFormula": "Functie | Functies", + "formulaTypeOperator": "Operator | Operators", + "formulaTypeData": "Data", + "formulaTypeDataEmpty": "Geen gegevensbronnen beschikbaar", + "categoryText": "Tekst", + "categoryNumber": "Nummer", + "categoryBoolean": "Boolean", + "categoryDate": "Datum", + "categoryCondition": "Voorwaarde" + }, + "formulaInputContext": { + "useRegularInput": "Gebruik regelmatige invoer", + "useAdvancedInput": "Geavanceerde invoer gebruiken", + "useRegularInputModalTitle": "Overschakelen op gewone invoer?", + "useAdvancedInputModalTitle": "Overschakelen naar geavanceerde invoer?", + "modalMessage": "Als u overschakelt naar een andere invoermodus, wordt de huidige formule gewist. Weet u zeker dat u wilt doorgaan?" + }, + "nodeExplorer": { + "noResults": "Geen resultaten gevonden", + "resetSearch": "Zoeken herstellen" } } diff --git a/web-frontend/modules/builder/locales/de.json b/web-frontend/modules/builder/locales/de.json index 8032ea01af..08d17ed795 100644 --- a/web-frontend/modules/builder/locales/de.json +++ b/web-frontend/modules/builder/locales/de.json @@ -87,14 +87,45 @@ "notAllowedInsideSameType": "Dieses Element ist nicht in einem Container der gleichen Art erlaubt", "dateTimePickerDescription": "Ein Eingabefeld für Datum und Zeit", "header": "Mehrseitiger Kopfbereich", - "notAllowedUnlessHeader": "Dieses Element ist nur im Kopfbereich der Seite erlaubt" + "notAllowedUnlessHeader": "Dieses Element ist nur im Kopfbereich der Seite erlaubt", + "ratingInput": "Bewertungseingabefeld", + "ratingInputDescription": "Ein Bewertungs-Eingabeelement", + "rating": "Bewertung", + "ratingDescription": "Ein Bewertungs-Element", + "invalidElementValue": "Ungültiger Elementwert: {value}", + "menu": "Menü", + "menuDescription": "Menü-Element", + "simpleContainer": "Container", + "simpleContainerDescription": "Ein Container für andere Elemente", + "fileInput": "Dateieingabe", + "fileInputDescription": "Ein Eingabefeld zum Hochladen von Dateien", + "errorValueMissing": "Fehlende Eigenschaft \"Wert\"", + "errorEmptyContainer": "Dieser Container ist leer", + "errorParentWithDataSourceMissing": "Keine Datenquelle für das Element oder einen übergeordneten Container ausgewählt", + "errorDataSourceMissing": "Keine Datenquelle ausgewählt", + "errorSchemaPropertyMissing": "Keine Schema-Eigenschaft ausgewählt", + "errorWorkflowActionInError": "Mindestens eine Aktion ist falsch konfiguriert", + "errorCollectionFieldInError": "Mindestens ein Feld ist falsch konfiguriert", + "errorNavigateToPageMissing": "Fehlende Eigenschaft \"Navigieren zu\"", + "errorPageParameterInError": "Mindestens ein Seitenparameter ist falsch konfiguriert", + "errorNavigationUrlMissing": "Fehlende Eigenschaft \"Navigations-URL\"", + "errorImageFileMissing": "Fehlende Bild-Datei", + "errorImageUrlMissing": "Fehlende Eigenschaft \"Bild-URL\"", + "errorNoWorkflowAction": "Keine Workflow-Aktion konfiguriert", + "errorOptionsMissing": "Keine Option konfiguriert", + "errorIframeUrlMissing": "Fehlende Eigenschaft \"IFrame-URL\"", + "errorIframeContentMissing": "Fehlender IFrame-Inhalt", + "errorNoMenuItem": "Kein Menüpunkt konfiguriert", + "errorMenuItemInError": "Mindestens ein Menü ist falsch konfiguriert", + "errorSubMenuItemInError": "Mindestens ein Untermenü ist falsch konfiguriert" }, "addElementButton": { "label": "Element" }, "duplicatePageJobType": { "duplicating": "Duplizieren", - "duplicatedTitle": "Seite dupliziert" + "duplicatedTitle": "Seite dupliziert", + "name": "Seite duplizieren" }, "pageHeaderItemTypes": { "labelElements": "Elemente", @@ -852,5 +883,24 @@ "imageConstraintFullWidth": "Auf maximale Breite ausweiten", "imageConstraintCover": "Cover", "imageConstraintContainDisabled": "Nicht verfügbar mit maximaler Höhe." + }, + "builderToast": { + "details": "Details", + "defaultTitle": "Unerwarteter Fehler", + "defaultMessage": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.", + "invalidContextTitle": "Ungültige Anfrage", + "invalidContextMessage": "Die Anfrage ist ungültig.", + "InvalidContentTitle": "Ungültiges Formular", + "invalidContentMessage": "Ein oder mehrere Felder enthalten ungültige Werte.", + "serviceMisconfiguredTitle": "Unvollständige Konfiguration", + "serviceMisconfiguredMessage": "Bitte prüfen Sie die Konfiguration.", + "errorWorkflowActionDispatch": "Die Aktion \"{name}\" ist fehlgeschlagen: ", + "errorDataSourceDispatch": "Fehler beim Laden der Daten von \"{name}\": " + }, + "addElementCategory": { + "suggestedElement": "Vorgeschlagene Elemente", + "baseElement": "Grundelemente", + "layoutElement": "Layout-Elemente", + "formElement": "Formularelemente" } } diff --git a/web-frontend/modules/builder/locales/fr.json b/web-frontend/modules/builder/locales/fr.json index a99525e135..baaea701a8 100644 --- a/web-frontend/modules/builder/locales/fr.json +++ b/web-frontend/modules/builder/locales/fr.json @@ -578,7 +578,8 @@ "tags": "Étiquettes", "button": "Bouton", "image": "Image", - "rating": "Notation" + "rating": "Notation", + "errorValueMissing": "Valeur manquante" }, "createUserSourceForm": { "userSourceName": "Nom", @@ -707,7 +708,10 @@ "errorFetchingRolesMessage": "Il y a eu un problème lors de la récupération des rôles d'utilisateur.", "rolesAllMembersOf": "Tous les membres de {name}", "noRole": "Sans rôle", - "errorFetchingRolesTitle": "Impossible de récupérer les rôles des utilisateurs" + "errorFetchingRolesTitle": "Impossible de récupérer les rôles des utilisateurs", + "visibilityCondition": "Conditions de visibilité", + "visibilityConditionHelper": "Si le résultat de cette formule est vrai et que le statut du visiteur ci-dessus est vrai, l'élément sera visible. Cette condition n'affecte que la visibilité de l'élément. Pour exclure des données de la réponse du serveur, utilisez l'option de filtrage du rôle de l'utilisateur ci-dessus.", + "visibilityConditionPlaceholder": "Condition..." }, "imageInput": { "labelDescription": "Description par défaut", diff --git a/web-frontend/modules/builder/locales/ko.json b/web-frontend/modules/builder/locales/ko.json index 5474514bad..7611cbbfc7 100644 --- a/web-frontend/modules/builder/locales/ko.json +++ b/web-frontend/modules/builder/locales/ko.json @@ -737,7 +737,8 @@ "link": "링크", "tags": "태그", "image": "이미지", - "rating": "평가" + "rating": "평가", + "errorValueMissing": "누락된 값 속성" }, "textFieldForm": { "fieldValueLabel": "값", @@ -863,7 +864,10 @@ "rolesAllMembersOf": "{name}의 모든 멤버", "noRole": "역할 없음", "errorFetchingRolesTitle": "사용자 역할을 가져올 수 없습니다", - "errorFetchingRolesMessage": "사용자 역할을 가져오는 동안 문제가 발생했습니다." + "errorFetchingRolesMessage": "사용자 역할을 가져오는 동안 문제가 발생했습니다.", + "visibilityCondition": "표시성 조건", + "visibilityConditionHelper": "이 수식의 결과가 참이고 위의 방문자 선택 사항이 참이면 요소가 표시됩니다. 이 조건은 요소의 표시 여부에만 영향을 미칩니다. 서버 응답에서 데이터를 제외하려면 위의 사용자 역할 필터링 옵션을 사용하세요.", + "visibilityConditionPlaceholder": "상태..." }, "userDataProviderType": { "isAuthenticated": "인증됨", diff --git a/web-frontend/modules/builder/locales/nl.json b/web-frontend/modules/builder/locales/nl.json index cbc3bc4c9f..38583930fc 100644 --- a/web-frontend/modules/builder/locales/nl.json +++ b/web-frontend/modules/builder/locales/nl.json @@ -543,7 +543,8 @@ "tags": "Tags", "button": "Knop", "image": "Afbeelding", - "rating": "Beoordeling" + "rating": "Beoordeling", + "errorValueMissing": "Ontbrekende waarde eigenschap" }, "createUserSourceForm": { "userSourceName": "Naam", @@ -705,7 +706,10 @@ "rolesAllMembersOf": "Alle leden van {name}", "noRole": "Geen rol", "errorFetchingRolesTitle": "Kan gebruikersrollen niet ophalen", - "errorFetchingRolesMessage": "Er is een probleem opgetreden tijdens het ophalen van gebruikersrollen." + "errorFetchingRolesMessage": "Er is een probleem opgetreden tijdens het ophalen van gebruikersrollen.", + "visibilityCondition": "Zichtbaarheid voorwaarde", + "visibilityConditionHelper": "Als het resultaat van deze formule waar is en de bezoekerskeuze hierboven waar is, zal het element zichtbaar zijn. Deze voorwaarde beïnvloedt alleen de zichtbaarheid van het element. Als u in plaats daarvan gegevens wilt uitsluiten van het serverantwoord, gebruikt u de bovenstaande filteroptie voor gebruikersrollen.", + "visibilityConditionPlaceholder": "Voorwaarde..." }, "imageInput": { "labelButton": "Uploaden", diff --git a/web-frontend/modules/core/locales/de.json b/web-frontend/modules/core/locales/de.json index 8e943f5313..a77a008455 100644 --- a/web-frontend/modules/core/locales/de.json +++ b/web-frontend/modules/core/locales/de.json @@ -967,7 +967,10 @@ "exportWorkspaceForm": { "onlyStructureLabel": "Nur die Struktur exportieren", "onlyStructureDescription": "Wenn diese Option aktiviert ist, wird nur die Struktur der Anwendungen exportiert. Die Daten werden nicht enthalten sein.", - "exportSettingsLabel": "Einstellungen exportieren" + "exportSettingsLabel": "Einstellungen exportieren", + "selectDataToExport": "Zu exportierende Daten auswählen", + "selectAll": "Alles auswählen", + "deselectAll": "Alle abwählen" }, "importWorkspaceModal": { "databases": "Datenbanken", @@ -992,6 +995,12 @@ "invalidResourceMessage": "Die bereitgestellte Datei ist kein gültiger Baserow-Export.", "untrustedPublicKeyTitle": "Nicht vertrauenswürdige Signatur", "importingState": "Importiere", - "title": "Baserow-Daten importieren" + "title": "Baserow-Daten importieren", + "uploadAndImport": "Hochladen & Importieren", + "importingTableStructure": "Erstelle: {table}", + "importingTableData": "Importiere Daten: {table}" + }, + "importWorkspaceForm": { + "selectApplicationsToImport": "Zu importierende Anwendungen auswählen" } } diff --git a/web-frontend/modules/core/locales/fr.json b/web-frontend/modules/core/locales/fr.json index 5ace004c5d..7406d3bb15 100644 --- a/web-frontend/modules/core/locales/fr.json +++ b/web-frontend/modules/core/locales/fr.json @@ -254,7 +254,8 @@ "emptyWorkspaceMessage": "Pour commencer, créez une nouvelle base de données ou une nouvelle application.", "addNew": "Ajouter...", "noWorkspace": "Aucun projet", - "noWorkspaceDescription": "Pour commencer, créez un nouveau projet" + "noWorkspaceDescription": "Pour commencer, créez un nouveau projet", + "starOnGitHub": "Suivez-nous sur GitHub" }, "login": { "title": "Bienvenue", @@ -536,7 +537,8 @@ "teams": "Équipes", "highestRole": "Rôle le plus élevé", "highestRoleHelpText": "Le rôle le plus élevé attribué à cet utilisateur, directement ou par une équipe, sur tout ce qui se trouve dans ce projet", - "highestRoleInstanceHelpText": "Le rôle le plus élevé dont dispose cet utilisateur dans l'instance" + "highestRoleInstanceHelpText": "Le rôle le plus élevé dont dispose cet utilisateur dans l'instance", + "2fa": "2FA" }, "title": "{userAmount} Membres dans {workspaceName}", "inviteMember": "Inviter un membre" @@ -663,7 +665,7 @@ "createTitle": "Nouvelle intégration" }, "integrationEditForm": { - "name": "Nom de l’intégration", + "name": "Nom", "namePlaceholder": "Saisissez le nom de l'intégration..." }, "workspaceInvitationCreatedNotification": { @@ -704,7 +706,8 @@ "noProvidersText": "Aucun fournisseur de données n'a été trouvé. Pour commencer, vous pouvez, par exemple, ajouter une source de données ou un paramètre de page." }, "formulaInputField": { - "errorInvalidFormula": "La formule est invalide." + "errorInvalidFormula": "La formule est invalide.", + "advancedFormulaMode": "Mode formule avancée" }, "userPasswordChangedToast": { "title": "Mot de passe modifié", @@ -770,7 +773,9 @@ "openRouterModelsDescription": "Fournissez une liste de [modèles OpenRouter](https://openrouter.ai/models) séparés par des virgules qui peuvent être utilisés dans Baserow. (par exemple `openai/gpt-4o,anthropic/claude-3-haiku`)", "openRouterApiKeyDescription": "Fournissez une clé API OpenRouter si vous souhaitez activer l'intégration. [Obtenir une clé API](https://openrouter.ai/settings/keys).", "openRouterApiKeyLabel": "Clé API", - "openRouterOrganization": "Organisation (facultatif)" + "openRouterOrganization": "Organisation (facultatif)", + "openaiBaseUrl": "URL de base", + "openaiBaseUrlDescription": "Utilise l'URL de base OpenAI par défaut si elle est vide. Elle peut être remplacée par https://eu.api.openai.com/v1, https://.openai.azure.com, ou toute autre API compatible avec OpenAI." }, "generativeAIWorkspaceSettings": { "title": "Paramètres de l'IA générative", @@ -859,7 +864,8 @@ "labelButton": "Envoyer" }, "dataExplorerNode": { - "showMore": "Afficher plus de répétitions" + "showMore": "Afficher plus de répétitions", + "selectNode": "Sélectionner" }, "dashboardApplication": { "createdAt": "créé(e)" @@ -1073,5 +1079,129 @@ }, "importWorkspaceForm": { "selectApplicationsToImport": "Sélectionnez les applications à importer" + }, + "workspaceSearch": { + "title": "Recherche", + "searchEverything": "Recherche...", + "searching": "Recherche en cours...", + "welcome": "Recherchez ce que vous souhaitez dans votre projet", + "welcomeSubtitle": "Utilisez le champ de recherche ci-dessus pour trouver des applications, des tables, des champs, etc.", + "navigate": "Voir", + "select": "Sélectionner", + "close": "Fermer", + "types": { + "applications": "Applications", + "tables": "Tables", + "fields": "Colonnes", + "rows": "Lignes" + }, + "noResults": "Aucun résultat trouvé", + "noResultsSubtitle": "Aucun résultat correspondant à \"{searchTerm}\"" + }, + "action": { + "cancel": "Annuler", + "submit": "Valider" + }, + "disableTwoFactorAuth": { + "successTitle": "L'authentification à deux facteurs a été désactivée", + "errorWrongPasswordTitle": "Mot de passe erroné", + "errorWrongPasswordMessage": "Le mot de passe saisi ne correspond pas à votre mot de passe.", + "title": "Êtes-vous sûr‑e de vouloir désactiver 2FA ?", + "description": "Votre compte va perdre une couche supplémentaire de sécurité. Si quelqu'un découvre votre mot de passe, ils pourraient être en mesure de se connecter à votre compte.", + "cancel": "Annuler", + "disable": "Désactiver" + }, + "enableTwoFactorOptions": { + "cancel": "Annuler", + "continue": "Continuer" + }, + "saveBackupCode": { + "description": "Si vous perdez l'accès à votre application d'authentification ou à votre téléphone et que vous ne pouvez pas recevoir ou générer des codes d'authentification, vous pouvez utiliser cette sauvegarde. Vous ne pouvez l'utiliser qu'une seule fois. Veillez à le noter ou à le copier dans un endroit sûr afin de pouvoir y accéder sans vous connecter.", + "backupCodes": "Codes de secours", + "copy": "Copier", + "continue": "Continuer", + "backupCodesCopiedTitle": "Copié !", + "backupCodesCopiedMessage": "Codes de secours copiés dans le presse-papiers." + }, + "totpAuthType": { + "name": "App d'authentification", + "description": "Utilisez une application pour obtenir des codes d'authentification à deux facteurs. Vous pouvez, par exemple, utiliser des applications telles que Google Authenticator, Authy et Microsoft Authenticator.", + "enabledDescription": "Vous recevrez des codes de vérification via une application d'authentification. Pour configurer une autre application ou méthode, il vous suffit de désactiver l'option 2FA et de la configurer à nouveau.", + "sideLabel": "Recommandé" + }, + "twoFactorEnabled": { + "enabled": "Activé", + "disable": "Désactiver 2FA" + }, + "totpLogin": { + "backupCodesTitle": "Saisir le code de secours", + "backupCodesDescription": "Connectez-vous avec votre code de secours à usage unique.", + "authenticate": "S'authentifier", + "goBack": "Retour", + "totpTitle": "Authentification à deux facteurs", + "totpDescription": "Saisissez le code de votre application d'authentification.", + "verify": "Vérifier", + "useBackupCode": "Utiliser le code de secours", + "verificationFailed": "Échec de la vérification", + "verificationFailedDescription": "Le code saisie n'est pas correct.", + "loginExpired": "Connexion expirée", + "loginExpiredDescription": "Veuillez indiquer de nouveau votre mot de passe.", + "rateLimit": "Trop de tentatives." + }, + "formulaInputContext": { + "variables": "Variables", + "functions": "Fonctions", + "operators": "Opérateurs", + "search": "Recherche", + "useRegularInputModalTitle": "Utiliser une formule simple ?", + "useRegularInput": "Utiliser une formule simple", + "useAdvancedInput": "Utiliser la saisie avancée", + "useAdvancedInputModalTitle": "Utiliser la saisie avancée pour ce champ ?", + "modalMessage": "Le contenu de votre champ sera effacé et il ne sera pas possible de le récupérer." + }, + "twoFactorAuthSettings": { + "title": "Authentification à deux facteurs", + "loadingError": "Impossible de charger la configuration de l'authentification à deux facteurs." + }, + "coreHTTPTriggerServiceForm": { + "title": "URL du webhook", + "urlVersion": "Version", + "urlVersionPublished": "Public", + "urlVersionTest": "Test", + "copyUrl": "Copier l'URL du Webhook", + "urlCopied": "URL du webhook copiée dans le presse-papiers", + "description": "Ce webhook recevra des requêtes HTTP et déclenchera le scénario avec les données de la requête comme payload.", + "methodsOptionLabel": "Méthodes HTTP autorisées", + "methodsOptionDescription": "Contrôle les méthodes HTTP autorisées pour ce webhook. L'exclusion de GET réduit le risque de déclenchement accidentel du webhook.", + "methodsOptionAll": "Toutes", + "methodsOptionExcludeGet": "Exclure GET" + }, + "twoFactorAuthEmpty": { + "title": "Vous n'avez pas encore activé 2FA", + "description": "Ajoutez une couche de sécurité supplémentaire à votre compte.", + "enable": "Activer 2FA", + "notAllowedTitle": "2FA non activé", + "notAllowedDescription": "L'ajout de 2FA n'est possible que pour les comptes basés sur un mot de passe." + }, + "nodeHelpTooltip": { + "exampleLabel": "Exemple", + "result": "Résultat : {result}" + }, + "enableWithQRCode": { + "scanQRCode": "Scanner un QR code", + "scanQRCodeDescription": "Scannez le code avec une application comme Google Authenticator, Authy ou Microsoft Authenticator.", + "clickToCopy": "Vous pouvez également cliquer ici pour copier le code.", + "secretCopiedTitle": "Secret copié", + "secretCopiedMessage": "Secret TOTP copié dans le presse-papiers.", + "enterCode": "Entrez le code indiqué", + "enterCodeDescription": "Saisissez un code à 6 chiffres affiché par l'application pour confirmer que vous l'avez configuré correctement.", + "verificationFailed": "Échec de la vérification", + "verificationFailedDescription": "Le code saisi n'est pas valide.", + "provisioningFailed": "Échec du provisionnement", + "checkSuccess": "L'authentification à deux facteurs a été activée avec succès" + }, + "twoFactorAuthField": { + "enabled": "Activé", + "disabled": "Désactivé" } } diff --git a/web-frontend/modules/core/locales/ko.json b/web-frontend/modules/core/locales/ko.json index 9397d0e6ba..43052095ed 100644 --- a/web-frontend/modules/core/locales/ko.json +++ b/web-frontend/modules/core/locales/ko.json @@ -258,7 +258,9 @@ "openRouterModelsDescription": "Baserow에서 사용할 수 있는 [OpenRouter 모델](https://openrouter.ai/models) 의 목록을 쉼표로 구분하여 제공하세요. (예 : `openai/gpt-4o,anthropic/claude-3-haiku`)", "openRouter": "OpenRouter", "openRouterOrganization": "조직 (선택 사항)", - "openRouterModelsLabel": "활성화된 모델" + "openRouterModelsLabel": "활성화된 모델", + "openaiBaseUrl": "기본 URL", + "openaiBaseUrlDescription": "비어 있으면 기본적으로 기본 OpenAI 기반 URL을 사용합니다. 선택적으로 https://eu.api.openai.com/v1, https://.openai.azure.com 또는 기타 OpenAI 호환 API로 변경할 수 있습니다." }, "generativeAIWorkspaceSettings": { "title": "생성형 AI 설정", @@ -439,7 +441,8 @@ "emptyWorkspaceMessage": "새 데이터베이스나 애플리케이션을 생성하여 시작하세요.", "addNew": "새로 추가...", "noWorkspace": "작업공간 없음", - "noWorkspaceDescription": "새 작업공간을 생성하여 시작하세요" + "noWorkspaceDescription": "새 작업공간을 생성하여 시작하세요", + "starOnGitHub": "GitHub에서 우리를 별표로 표시하세요" }, "dashboardApplication": { "createdAt": "생성됨" @@ -636,7 +639,8 @@ "highestRole": "최고 역할", "highestRoleHelpText": "이 작업공간의 어떤 것에서 직접 또는 팀에서 할당된 이 사용자의 최고 역할", "teams": "팀", - "highestRoleInstanceHelpText": "인스턴스에서 이 사용자가 갖는 가장 높은 역할" + "highestRoleInstanceHelpText": "인스턴스에서 이 사용자가 갖는 가장 높은 역할", + "2fa": "2FA" }, "actions": { "copyEmail": "이메일 복사", @@ -694,7 +698,7 @@ "warningTitle": "경고" }, "integrationEditForm": { - "name": "통합 이름", + "name": "이름", "namePlaceholder": "통합 이름 입력..." }, "integrationDropdown": { @@ -735,7 +739,8 @@ "submitButton": "빈도 업데이트" }, "formulaInputField": { - "errorInvalidFormula": "공식이 유효하지 않습니다." + "errorInvalidFormula": "공식이 유효하지 않습니다.", + "advancedFormulaMode": "고급 수식 모드" }, "dataExplorer": { "noMatchingNodesText": "일치하는 결과가 없습니다.", @@ -820,7 +825,8 @@ "labelButton": "업로드" }, "dataExplorerNode": { - "showMore": "더 많은 반복 표시" + "showMore": "더 많은 반복 표시", + "selectNode": "선택" }, "user": { "isStaff": "직원 여부", @@ -976,5 +982,129 @@ }, "importWorkspaceForm": { "selectApplicationsToImport": "가져올 애플리케이션을 선택하세요" + }, + "workspaceSearch": { + "title": "검색", + "searchEverything": "검색...", + "searching": "검색 중...", + "noResults": "검색 결과가 없습니다", + "noResultsSubtitle": "\"{searchTerm}\"와(과) 일치하는 항목을 찾을 수 없습니다", + "welcome": "작업 공간의 모든 것을 검색하세요", + "welcomeSubtitle": "위의 검색창을 사용하여 애플리케이션, 테이블, 필드 등을 찾아보세요", + "navigate": "탐색", + "select": "선택", + "close": "닫기", + "types": { + "applications": "애플리케이션", + "tables": "테이블", + "fields": "필드", + "rows": "행" + } + }, + "action": { + "cancel": "취소", + "submit": "제출" + }, + "coreHTTPTriggerServiceForm": { + "title": "웹훅 URL", + "urlVersion": "버전", + "urlVersionPublished": "게시됨", + "urlVersionTest": "테스트", + "copyUrl": "웹훅 URL 복사", + "urlCopied": "웹훅 URL이 클립보드에 복사되었습니다", + "description": "이 웹훅은 HTTP 요청을 수신하고 요청 데이터를 페이로드로 사용하여 워크플로를 트리거합니다.", + "methodsOptionLabel": "허용된 HTTP 메서드", + "methodsOptionDescription": "이 웹훅에 허용되는 HTTP 메서드를 제어합니다. GET을 제외하면 웹훅이 실수로 트리거될 가능성이 줄어듭니다.", + "methodsOptionAll": "전체", + "methodsOptionExcludeGet": "GET 제외" + }, + "twoFactorAuthSettings": { + "title": "2단계 인증", + "loadingError": "2단계 인증을 로드할 수 없습니다." + }, + "disableTwoFactorAuth": { + "title": "2FA를 비활성화하시겠습니까?", + "description": "계정 보안이 한 단계 더 약화됩니다. 누군가 비밀번호를 알아내면 계정에 로그인할 수 있습니다.", + "cancel": "그대로 두세요", + "disable": "비활성화", + "successTitle": "2단계 인증이 비활성화되었습니다", + "errorWrongPasswordTitle": "잘못된 비밀번호", + "errorWrongPasswordMessage": "입력한 비밀번호가 귀하의 비밀번호와 일치하지 않습니다." + }, + "enableTwoFactorOptions": { + "cancel": "취소", + "continue": "계속" + }, + "saveBackupCode": { + "description": "인증 앱이나 휴대폰에 접근할 수 없어 인증 코드를 수신하거나 생성할 수 없는 경우 이 백업 코드를 사용할 수 있습니다. 이 코드는 한 번만 사용 가능합니다. 로그인 없이도 접근할 수 있도록 반드시 기록하거나 안전한 장소에 복사해 두십시오.", + "backupCodes": "백업 코드", + "copy": "복사", + "continue": "계속", + "backupCodesCopiedTitle": "복사됨!", + "backupCodesCopiedMessage": "백업 코드가 클립보드에 복사되었습니다." + }, + "totpAuthType": { + "name": "인증 앱", + "description": "앱을 사용하여 2단계 인증 코드를 받으세요. Google Authenticator, Authy, Microsoft Authenticator 등의 앱을 사용하는 것이 좋습니다.", + "enabledDescription": "인증 앱을 통해 인증 코드를 받게 됩니다. 다른 앱이나 인증 방식을 설정하려면 2FA를 비활성화하고 다시 설정하세요.", + "sideLabel": "추천" + }, + "twoFactorEnabled": { + "enabled": "활성화됨", + "disable": "2FA 비활성화" + }, + "totpLogin": { + "backupCodesTitle": "백업 코드를 입력하세요", + "backupCodesDescription": "일회용 백업 코드로 로그인하세요.", + "authenticate": "인증", + "goBack": "돌아가기", + "totpTitle": "2단계 인증", + "totpDescription": "인증 앱에서 코드를 입력하세요.", + "verify": "확인", + "useBackupCode": "백업 코드 사용", + "verificationFailed": "검증에 실패했습니다", + "verificationFailedDescription": "입력한 코드가 올바르지 않습니다.", + "loginExpired": "로그인이 만료되었습니다", + "loginExpiredDescription": "비밀번호를 다시 입력해 주세요.", + "rateLimit": "시도가 너무 많습니다." + }, + "formulaInputContext": { + "variables": "변수", + "functions": "기능", + "operators": "연산자", + "search": "검색", + "useRegularInputModalTitle": "이 필드에 일반 입력을 사용하시겠습니까?", + "useRegularInput": "일반적인 입력을 사용", + "useAdvancedInput": "고급 입력 사용", + "useAdvancedInputModalTitle": "이 필드에 고급 입력을 사용하시겠습니까?", + "modalMessage": "해당 필드의 내용이 삭제되며 복구할 수 없습니다." + }, + "twoFactorAuthField": { + "enabled": "활성화됨", + "disabled": "비활성화됨" + }, + "twoFactorAuthEmpty": { + "title": "아직 2FA를 활성화하지 않았습니다", + "description": "귀하의 계정에 보안을 한층 더 강화하세요.", + "enable": "2FA 활성화", + "notAllowedTitle": "2FA가 활성화되지 않았습니다", + "notAllowedDescription": "2FA 추가는 비밀번호 기반 계정에만 가능합니다." + }, + "nodeHelpTooltip": { + "exampleLabel": "예시", + "result": "결과: {result}" + }, + "enableWithQRCode": { + "scanQRCode": "QR코드 스캔", + "scanQRCodeDescription": "Google Authenticator, Authy 또는 Microsoft Authenticator와 같은 앱으로 코드를 스캔하세요.", + "clickToCopy": "또는 여기를 클릭하여 코드를 복사하세요.", + "secretCopiedTitle": "시크릿 복사됨", + "secretCopiedMessage": "TOTP 비밀번호가 클립보드에 복사되었습니다.", + "enterCode": "표시된 코드를 입력하세요", + "enterCodeDescription": "앱에 표시된 6자리 코드를 입력하여 올바르게 설정되었는지 확인하세요.", + "verificationFailed": "검증에 실패했습니다", + "verificationFailedDescription": "입력한 코드가 유효하지 않습니다.", + "provisioningFailed": "프로비저닝에 실패했습니다", + "checkSuccess": "2단계 인증을 성공적으로 활성화했습니다" } } diff --git a/web-frontend/modules/core/locales/nl.json b/web-frontend/modules/core/locales/nl.json index 2229f6f63d..8cc2bee20e 100644 --- a/web-frontend/modules/core/locales/nl.json +++ b/web-frontend/modules/core/locales/nl.json @@ -254,7 +254,8 @@ "tutorialsMessage": "Ontdek het potentieel van Baserow", "emptyWorkspaceMessage": "Begin met het maken van een nieuwe database of applicatie.", "addNew": "Nieuwe toevoegen...", - "noWorkspace": "Geen werkruimte" + "noWorkspace": "Geen werkruimte", + "starOnGitHub": "Geef een ster op GitHub" }, "login": { "title": "Inloggen", @@ -509,7 +510,8 @@ "teams": "Teams", "highestRole": "Hoogste rol", "highestRoleHelpText": "De hoogste rol die deze gebruiker rechtstreeks of vanuit een team aan hem is toegekend, op alles in deze werkruimte", - "highestRoleInstanceHelpText": "De hoogste rol die deze gebruiker heeft in de instantie" + "highestRoleInstanceHelpText": "De hoogste rol die deze gebruiker heeft in de instantie", + "2fa": "2FA" }, "actions": { "copyEmail": "E-mail kopiëren", @@ -704,7 +706,8 @@ "noMatchingNodesText": "Er zijn geen overeenkomende gegevensverstrekkers gevonden." }, "formulaInputField": { - "errorInvalidFormula": "De formule is ongeldig." + "errorInvalidFormula": "De formule is ongeldig.", + "advancedFormulaMode": "Geavanceerde formule-modus" }, "userPasswordChangedToast": { "title": "Wachtwoord veranderd", @@ -767,7 +770,9 @@ "openRouter": "OpenRouter", "openRouterModelsLabel": "Ingeschakelde modellen", "openRouterModelsDescription": "Geef een lijst met door komma's gescheiden [OpenRouter modellen] (https://openrouter.ai/models) die kunnen worden gebruikt in Baserow. (bijv. `openai/gpt-4o,anthropic/claude-3-haiku`)", - "openRouterApiKeyDescription": "Geef een OpenRouter API-key op als u de integratie wilt inschakelen. [get an API key](https://openrouter.ai/settings/keys)." + "openRouterApiKeyDescription": "Geef een OpenRouter API-key op als u de integratie wilt inschakelen. [get an API key](https://openrouter.ai/settings/keys).", + "openaiBaseUrl": "Basis URL", + "openaiBaseUrlDescription": "Gebruikt standaard de OpenAI basis URL als deze leeg is. Kan optioneel worden gewijzigd in https://eu.api.openai.com/v1, https://.openai.azure.com, of een andere OpenAI-compatibele API." }, "generativeAIWorkspaceSettings": { "title": "Generatieve AI-instellingen", @@ -865,7 +870,8 @@ "viewMore": "Meer bekijken" }, "dataExplorerNode": { - "showMore": "Meer herhalingen tonen" + "showMore": "Meer herhalingen tonen", + "selectNode": "Selecteer" }, "exportWorkspaceModal": { "exportSettings": "Export instellingen", @@ -1074,5 +1080,129 @@ }, "importWorkspaceForm": { "selectApplicationsToImport": "Applicaties selecteren om te importeren" + }, + "workspaceSearch": { + "title": "Zoek", + "searchEverything": "Zoek...", + "searching": "Zoeken...", + "noResults": "Geen resultaten gevonden", + "noResultsSubtitle": "We konden niets vinden dat overeenkomt met \"{searchTerm}\"", + "welcome": "Doorzoek alles in je werkruimte", + "welcomeSubtitle": "Gebruik het zoekvak hierboven om toepassingen, tabellen, velden en meer te vinden", + "navigate": "Navigatie", + "select": "Selecteer", + "close": "Sluit", + "types": { + "applications": "Applicaties", + "tables": "Tabellen", + "fields": "Velden", + "rows": "Rijen" + } + }, + "action": { + "cancel": "Annuleren", + "submit": "Verstuur" + }, + "twoFactorAuthSettings": { + "title": "Tweefactorauthenticatie", + "loadingError": "Kon tweefactorconfiguratie niet laden." + }, + "disableTwoFactorAuth": { + "title": "Weet je zeker dat je 2FA wilt uitschakelen?", + "description": "Je account verliest een extra beveiligingslaag. Als iemand achter je wachtwoord komt, kan hij of zij misschien inloggen op je account.", + "cancel": "Laat het aan", + "disable": "Uitschakelen", + "successTitle": "Tweefactorauthenticatie is uitgeschakeld", + "errorWrongPasswordTitle": "Verkeerd wachtwoord", + "errorWrongPasswordMessage": "Het ingevoerde wachtwoord komt niet overeen met uw wachtwoord." + }, + "enableTwoFactorOptions": { + "cancel": "Annuleren", + "continue": "Ga verder" + }, + "saveBackupCode": { + "description": "Als je de toegang tot de authenticator-app of telefoon verliest en geen verificatiecodes kunt ontvangen of genereren, kun je deze back-up gebruiken. Je kunt het maar één keer gebruiken. Zorg ervoor dat je het opschrijft of kopieert naar een veilige plek zodat je het kunt openen zonder in te loggen.", + "backupCodes": "Back-up codes", + "copy": "Kopiëren", + "continue": "Ga verder", + "backupCodesCopiedTitle": "Gekopieerd!", + "backupCodesCopiedMessage": "Back-upcodes gekopieerd naar klembord." + }, + "totpAuthType": { + "name": "Authenticator-app", + "description": "Gebruik een app om tweefactorauthenticatiecodes te krijgen. We raden aan apps te gebruiken zoals Google Authenticator, Authy en Microsoft Authenticator.", + "enabledDescription": "Je ontvangt verificatiecodes via een authenticatie-app. Om een andere app of methode in te stellen, schakel je gewoon 2FA uit en stel je opnieuw in.", + "sideLabel": "Aanbevolen" + }, + "twoFactorEnabled": { + "enabled": "Ingeschakeld", + "disable": "2FA uitschakelen" + }, + "totpLogin": { + "backupCodesTitle": "Back-upcode invoeren", + "backupCodesDescription": "Log in met je eenmalige back-upcode.", + "authenticate": "Authenticeren", + "goBack": "Ga terug", + "totpTitle": "Tweefacturauthenticatie", + "totpDescription": "Voer de code van je authenticator-app in.", + "verify": "Controleer", + "useBackupCode": "Gebruik back-upcode", + "verificationFailed": "Verificatie mislukt", + "verificationFailedDescription": "De ingevoerde code is niet correct.", + "loginExpired": "Inloggen verlopen", + "loginExpiredDescription": "Geef je wachtwoord opnieuw op.", + "rateLimit": "Te veel pogingen." + }, + "formulaInputContext": { + "variables": "Variables", + "functions": "Functies", + "operators": "Operators", + "search": "Zoeken", + "useRegularInputModalTitle": "Normale invoer gebruiken voor dit veld?", + "useRegularInput": "Gebruik regelmatige invoer", + "useAdvancedInput": "Geavanceerde invoer gebruiken", + "useAdvancedInputModalTitle": "Geavanceerde invoer gebruiken voor dit veld?", + "modalMessage": "De inhoud van je veld zal worden gewist en het zal niet mogelijk zijn om het te herstellen." + }, + "coreHTTPTriggerServiceForm": { + "title": "Webhook URL", + "urlVersion": "Versie", + "urlVersionPublished": "Gepubliceerd", + "urlVersionTest": "Test", + "copyUrl": "URL webhook kopiëren", + "urlCopied": "Webhook URL gekopieerd naar klembord", + "description": "Deze webhook zal HTP verzoeken ontvangen en de workflow starten met de verzoekgegevens als de lading.", + "methodsOptionLabel": "Toegestane HTTP-methoden", + "methodsOptionDescription": "Bepaalt welke HTTP-methodes zijn toegestaan voor deze webhook. Het uitsluiten van GET vermindert de kans dat de webhook per ongeluk wordt geactiveerd.", + "methodsOptionAll": "Alle", + "methodsOptionExcludeGet": "GET uitsluiten" + }, + "twoFactorAuthEmpty": { + "title": "Je hebt 2FA nog niet ingeschakeld", + "description": "Voeg een extra beveiligingslaag toe aan je account.", + "enable": "2FA inschakelen", + "notAllowedTitle": "2FA niet ingeschakeld", + "notAllowedDescription": "Het toevoegen van 2FA is alleen mogelijk voor accounts met een wachtwoord." + }, + "nodeHelpTooltip": { + "exampleLabel": "Voorbeeld", + "result": "Resultaat: {result}" + }, + "enableWithQRCode": { + "scanQRCode": "QR-code scannen", + "scanQRCodeDescription": "Scan de code met een app zoals Google Authenticator, Authy of Microsoft Authenticator.", + "clickToCopy": "Je kunt ook hier klikken om de code te kopiëren.", + "secretCopiedTitle": "Geheim gekopieerd", + "secretCopiedMessage": "TOTP geheim gekopieerd naar klembord.", + "enterCode": "Voer de getoonde code in", + "enterCodeDescription": "Voer een 6-cijferige code in die door de app wordt getoond om te bevestigen dat je de app correct hebt ingesteld.", + "verificationFailed": "Verificatie mislukt", + "verificationFailedDescription": "De ingevoerde code is ongeldig.", + "provisioningFailed": "Voorziening mislukt", + "checkSuccess": "Tweefactorauthenticatie succesvol ingeschakeld" + }, + "twoFactorAuthField": { + "enabled": "Ingeschakeld", + "disabled": "Uitgeschakeld" } } diff --git a/web-frontend/modules/database/locales/fr.json b/web-frontend/modules/database/locales/fr.json index 3469a634d9..99a444d263 100644 --- a/web-frontend/modules/database/locales/fr.json +++ b/web-frontend/modules/database/locales/fr.json @@ -276,7 +276,8 @@ "dbIndex": "Index", "dbIndexError": "Ce type de champ ne peut pas avoir d'index. Veuillez le supprimer avant d'enregistrer ou de modifier le type de champ.", "dbIndexDescription": "L'indexation peut améliorer considérablement les performances de filtrage, mais ralentit les opérations de création, de mise à jour et de suppression.", - "defaultValueDisabledByConstraint": "Impossible de définir une valeur par défaut avec une contrainte d'unicité" + "defaultValueDisabledByConstraint": "Impossible de définir une valeur par défaut avec une contrainte d'unicité", + "dbIndexDisabledTooltip": "L'indexation n'est pas disponible pour ce type de champ." }, "fieldLookupSubForm": { "noTable": "Vous devez créer au moins une autre table dans la même base de données pour pouvoir créer un lien.", @@ -560,7 +561,8 @@ "generateCellsValues": "Générer des valeurs avec l'IA", "AIValuesGenerationErrorTitle": "La génération de valeur par l'IA a échoué", "AIValuesGenerationErrorMessage": "Veuillez vérifier votre API_KEY et le modèle sélectionné.", - "copyCellsWithHeader": "Copier les cellules avec l'en-tête" + "copyCellsWithHeader": "Copier les cellules avec l'en-tête", + "generateAllAiValues": "Générer toutes les valeurs" }, "gridViewFieldLinkRow": { "unnamed": "Ligne sans nom {value}", @@ -873,7 +875,7 @@ "selectTargetFieldLabel": "Sélectionner un champ à cumuler" }, "fieldSelectThroughFieldSubForm": { - "noTable": "Vous avez besoin d’au moins un champ te type « lien vers une table » pour créer ce champ.", + "noTable": "Vous avez besoin d’au moins un champ de type « lien vers une table » pour créer ce champ.", "selectThroughFieldLabel": "Sélectionner un champ « lien vers une table »" }, "apiDocsFiltersBuilderModal": { @@ -907,7 +909,8 @@ }, "collaboratorAddedToRowNotification": { "title": "{sender} vous a assigné à {fieldName} dans la ligne {rowId} de la table {tableName}", - "deletedUser": "Un utilisateur supprimé" + "deletedUser": "Un utilisateur supprimé", + "unknownUser": "Un utilisateur inconnu" }, "fieldCollaboratorSubForm": { "notifyUserWhenAdded": "Notifier l'utilisateur lorsqu'il est ajouté" diff --git a/web-frontend/modules/database/locales/ko.json b/web-frontend/modules/database/locales/ko.json index bc2edc0940..02be6e840a 100644 --- a/web-frontend/modules/database/locales/ko.json +++ b/web-frontend/modules/database/locales/ko.json @@ -308,7 +308,8 @@ "dbIndex": "색인", "dbIndexError": "이 필드 유형에는 인덱스가 있을 수 없습니다. 필드 유형을 저장하거나 변경하기 전에 인덱스를 제거하세요.", "dbIndexDescription": "인덱싱을 사용하면 필터링 성능이 크게 향상되지만 생성, 업데이트 및 삭제 작업 속도가 느려집니다.", - "defaultValueDisabledByConstraint": "고유 제약 조건으로 기본값을 설정할 수 없습니다" + "defaultValueDisabledByConstraint": "고유 제약 조건으로 기본값을 설정할 수 없습니다", + "dbIndexDisabledTooltip": "이 필드 유형에는 인덱싱을 사용할 수 없습니다." }, "fieldSelectThroughFieldSubForm": { "noTable": "이 필드를 생성하려면 최소 하나의 테이블 링크 필드가 필요합니다.", @@ -670,7 +671,8 @@ "tooManyItemsTitle": "항목이 너무 많음", "tooManyItemsDescription": "한 번에 {limit}개 이상의 행을 업데이트할 수 없으므로 처음 몇 개만 업데이트했습니다.", "AIValuesGenerationErrorTitle": "AI 값 생성 실패", - "AIValuesGenerationErrorMessage": "API_KEY를 확인하고 선택한 모델을 확인하세요." + "AIValuesGenerationErrorMessage": "API_KEY를 확인하고 선택한 모델을 확인하세요.", + "generateAllAiValues": "모든 AI 값 생성" }, "gridViewFieldFile": { "dropHere": "여기에 놓기", diff --git a/web-frontend/modules/database/locales/nl.json b/web-frontend/modules/database/locales/nl.json index 7974ef0d3f..c0e59cb0c0 100644 --- a/web-frontend/modules/database/locales/nl.json +++ b/web-frontend/modules/database/locales/nl.json @@ -276,7 +276,8 @@ "dbIndex": "Index", "dbIndexError": "Dit veldtype kan geen index hebben. Verwijder deze voordat u het veldtype opslaat of wijzigt.", "dbIndexDescription": "Indexeren kan de filterprestaties aanzienlijk verbeteren, maar vertraagt het maken, bijwerken en verwijderen.", - "defaultValueDisabledByConstraint": "Kan geen standaardwaarde instellen met een unieke beperking" + "defaultValueDisabledByConstraint": "Kan geen standaardwaarde instellen met een unieke beperking", + "dbIndexDisabledTooltip": "Indexering is niet beschikbaar voor dit veldtype." }, "fieldLookupSubForm": { "noTable": "Je hebt minstens één link-rij veld nodig om een lookup veld te maken.", @@ -560,7 +561,8 @@ "generateCellsValues": "Genereer waardes met AI", "AIValuesGenerationErrorTitle": "AI-waardegeneratie mislukt", "AIValuesGenerationErrorMessage": "Controleer alsjeblieft je API_KEY en controleer het geselecteerde model.", - "copyCellsWithHeader": "Cellen met koptekst kopiëren" + "copyCellsWithHeader": "Cellen met koptekst kopiëren", + "generateAllAiValues": "Genereer alle AI-waarden" }, "gridViewFieldLinkRow": { "unnamed": "onbenoemde rij {value}", @@ -907,7 +909,8 @@ }, "collaboratorAddedToRowNotification": { "title": "{sender} heeft u toegewezen aan {fieldName} in rij {rowId} in {tableName}", - "deletedUser": "Een verwijderde gebruiker" + "deletedUser": "Een verwijderde gebruiker", + "unknownUser": "Een onbekende gebruiker" }, "fieldCollaboratorSubForm": { "notifyUserWhenAdded": "Gebruiker op de hoogte stellen wanneer toegevoegd" diff --git a/web-frontend/modules/integrations/locales/de.json b/web-frontend/modules/integrations/locales/de.json index 73e8f194be..4c1705088c 100644 --- a/web-frontend/modules/integrations/locales/de.json +++ b/web-frontend/modules/integrations/locales/de.json @@ -45,14 +45,50 @@ "integrationType": { "localBaserow": "Lokales Baserow", "localBaserowSummary": "Lokales Baserow - {name}, {username}", - "localBaserowWarning": "Wenn Sie Ihr Konto autorisieren, hat jeder, der Bearbeitungsrechte für die Anwendung hat, vollen Zugriff auf die Daten, auf die auch Sie Zugriff haben. Es ist möglich, einen zweiten Benutzer zu erstellen, ihm die notwendigen Berechtigungen zu geben und stattdessen diesen zu verwenden." + "localBaserowWarning": "Wenn Sie Ihr Konto autorisieren, hat jeder, der Bearbeitungsrechte für die Anwendung hat, vollen Zugriff auf die Daten, auf die auch Sie Zugriff haben. Es ist möglich, einen zweiten Benutzer zu erstellen, ihm die notwendigen Berechtigungen zu geben und stattdessen diesen zu verwenden.", + "smtp": "SMTP-E-Mail" }, "serviceType": { "localBaserowGetRow": "Einzelne Zeile abfragen", "localBaserowListRows": "Mehrere Zeilen auflisten", "localBaserowAggregateRows": "Feld zusammenfassen", "misconfigured": "Fehlkonfiguriert", - "trashedField": "Verworfenes Feld" + "trashedField": "Verworfenes Feld", + "localBaserowGetRowDescription": "Eine einzelne Zeile aus einer Baserow-Tabelle lesen.", + "localBaserowListRowsDescription": "Liest mehrere Zeilen aus einer Baserow-Tabelle.", + "localBaserowAggregateRowsDescription": "Fasst ein Feld in einer Baserow-Tabelle zusammen.", + "localBaserowCreateRow": "Eine Zeile erstellen", + "localBaserowCreateRowDescription": "Erstellt eine neue Zeile in einer Baserow-Tabelle.", + "localBaserowUpdateRow": "Eine Zeile aktualisieren", + "localBaserowUpdateRowDescription": "Aktualisiert eine bestehende Zeile in einer Baserow-Tabelle.", + "localBaserowDeleteRow": "Eine Zeile löschen", + "localBaserowDeleteRowDescription": "Löscht eine Zeile in einer Baserow-Tabelle.", + "localBaserowRowsCreated": "Zeilen werden erstellt", + "localBaserowRowsCreatedDescription": "Wird ausgelöst, wenn Zeilen in einer Baserow-Tabelle erstellt werden.", + "localBaserowRowsUpdated": "Zeilen werden aktualisiert", + "localBaserowRowsUpdatedDescription": "Wird ausgelöst, wenn Zeilen in einer Baserow-Tabelle aktualisiert werden.", + "localBaserowRowsDeleted": "Zeilen werden gelöscht", + "localBaserowRowsDeletedDescription": "Wird ausgelöst, wenn Zeilen in einer Baserow-Tabelle gelöscht werden.", + "coreHTTPRequest": "Eine HTTP-Anfrage senden", + "coreHTTPRequestDescription": "Sendet eine HTTP-Anfrage an einen angegebenen Endpunkt.", + "coreSMTPEmail": "E-Mail versenden", + "coreRouter": "Router", + "coreRouterEdgesRequired": "Mindestens ein Edge ist erforderlich", + "coreRouterEdgeLabelRequired": "Eine Zweigbezeichnung ist erforderlich.", + "coreRouterEdgeConditionRequired": "Eine Zweigbedingung ist erforderlich.", + "coreRouterDescription": "Leitet den Workflow basierend auf einer Bedingung zum nächsten Zweig weiter.", + "coreSMTPEmailDescription": "Sendet eine E-Mail unter Verwendung der SMTP-Konfiguration.", + "errorFromEmailMissing": "Fehlende E-Mail-Eigenschaft \"From\"", + "errorToEmailsMissing": "Fehlende E-Mail-Eigenschaft \"To\"", + "errorUrlMissing": "Fehlende URL-Eigenschaft", + "errorNoTableSelected": "Keine Tabelle ausgewählt", + "errorFilterInError": "Mindestens ein Filter ist falsch konfiguriert", + "errorSortingInError": "Midestens eine Sortierung ist falsch konfiguriert", + "errorNoFieldSelected": "Kein Feld ausgewählt", + "errorNoAggregationTypeSelected": "Kein Aggregationstyp ausgewählt", + "corePeriodic": "Periodischer Auslöser", + "corePeriodicDescription": "Löst den Workflow in regelmäßigen Abständen zu festgelegten Zeitpunkten aus", + "corePeriodicErrorIntervalMissing": "Ein Intervall ist erforderlich." }, "userSourceType": { "localBaserow": "Baserow-Tabellen-Authentifizierung" @@ -67,7 +103,11 @@ "filterTabTitle": "Filter", "noTableChosenForSorting": "Wählen Sie eine Tabelle um die Datenquellen-Sortierung zu nutzen.", "searchFieldPlaceHolder": "Geben Sie einen Suchbegriff ein...", - "noTableChosenForFiltering": "Wählen Sie eine Tabelle um die Filter der Datenquelle nutzen zu können." + "noTableChosenForFiltering": "Wählen Sie eine Tabelle um die Filter der Datenquelle nutzen zu können.", + "advancedConfig": "Erweitert", + "defaultResultCount": "Standardanzahl der Ergebnisse", + "defaultResultCountHelp": "Die Standardanzahl der Datensätze, die diese Datenquelle auf Ihrer Seite abruft. Wenn Sie den Wert auf 0 setzen und ihn für ein Sammelelement verwenden, wird die Leistung beim Laden der Seite verbessert, da die Datensätze nur abgerufen werden, wenn das Element paginiert ist.", + "defaultResultCountPlaceholder": "Standardwert..." }, "localBaserowIntegrationType": { "localBaserowSummary": "Lokales Baserow - {name}, {username}", @@ -85,5 +125,103 @@ "aggregationTypeLabel": "Aggregierung", "searchFieldPlaceHolder": "Geben Sie einen Suchbegriff ein...", "noTableChosenForFiltering": "Wählen Sie eine Tabelle, um mit der Verwendung von Datenquellenfiltern zu beginnen." + }, + "serviceRefinementForms": { + "filterTabTitle": "Filter | 1 Filter | {count} Filter", + "sortTabTitle": "Sortierung | 1 Sortierung | {count} Sortierungen", + "searchTabTitle": "Suche | 1 Suche | {count} Suchen", + "searchFieldPlaceHolder": "Geben Sie einen Suchbegriff ein...", + "noTableChosenForFiltering": "Wählen Sie eine Tabelle aus, um mit der Verwendung von Datenquellen-Filtern zu beginnen.", + "noTableChosenForSorting": "Wählen Sie eine Tabelle aus, um mit der Verwendung von Datenquellen-Sortierungen zu beginnen.", + "refinements": "Verfeinerungen" + }, + "coreHTTPRequestServiceForm": { + "httpMethod": "HTTP-Methode", + "url": "Endpunkt-URL", + "queryParams": "Abfrageparameter", + "name": "Name", + "value": "Wert", + "namePlaceholder": "Name...", + "valuePlaceholder": "Wert...", + "headers": "Kopfzeilen", + "addQueryParam": "Abfrageparameter hinzufügen", + "addHeader": "Kopfzeile hinzufügen", + "formData": "Formulardaten", + "addFormData": "Formular-Daten hinzufügen", + "bodyType": "Body-Typ", + "bodyContent": "Body-Inhalt", + "urlPlaceholder": "Endpunkt-URL eingeben...", + "bodyPlaceholder": "Anfrage-Body hinzufügen...", + "timeoutPlaceholder": "Zeitlimit eingeben...", + "nameFieldRequired": "Die Eigenschaft \"name\" ist erforderlich.", + "seconds": "Sekunden", + "nameFieldInvalid": "Der Name darf nur alphanumerische Zeichen, Bindestriche oder Unterstriche enthalten und darf nicht mit einem Bindestrich oder einem Unterstrich beginnen.", + "timeout": "Zeitlimit" + }, + "smtpIntegrationType": { + "smtpSummary": "SMTP - {host}:{port}" + }, + "smtpForm": { + "host": "SMTP-Server", + "hostPlaceholder": "smtp.email.com", + "port": "SMTP-Port", + "portPlaceholder": "587", + "useTls": "TLS verwenden", + "username": "Benutzername", + "usernamePlaceholder": "deine-email-adresse@beispiel.de", + "password": "Passwort", + "passwordPlaceholder": "swordfish" + }, + "smtpEmailForm": { + "integrationDropdownLabel": "Integration", + "fromEmail": "Absender-E-Mailadresse", + "fromEmailPlaceholder": "absender@beispiel.de", + "fromName": "Absendername", + "fromNamePlaceholder": "Ihr Name", + "toEmails": "Empfänger-E-Mailadressen", + "toEmailsPlaceholder": "empfaenger1@beispiel.de,empfaenger2@beispiel.de", + "ccEmails": "CC-E-Mailempfänger", + "ccEmailsPlaceholder": "cc1@beispiel.de,cc2@beispiel.de", + "bccEmails": "BCC-E-Mailempfänger", + "bccEmailsPlaceholder": "bcc1@beispiel.de,bcc2@beispiel.de", + "subject": "Betreff", + "subjectPlaceholder": "Ihr E-Mailbetreff", + "bodyType": "Body-Typ", + "bodyTypePlain": "Klartext", + "bodyTypeHtml": "HTML", + "body": "Body", + "bodyPlaceholder": "Ihr E-Mailinhalt..." + }, + "routerForm": { + "defaultEdgeLabelLabel": "Standard-Zweigbezeichnung", + "defaultEdgeLabelPlaceholder": "Standard", + "defaultEdgeLabelDescription": "Wählen Sie optional das Label aus, das auf dem Standardzweig angezeigt werden soll.", + "branchesHeading": "Verzweigungen", + "branchesDescription": "Mit Verzweigungen können Sie Knoten nur ausführen, wenn eine Bedingung erfüllt ist. Verzweigungen werden von links nach rechts ausgeführt, bis eine Bedingung erfüllt ist. Andernfalls wird die Standardverzweigung ausgeführt. ", + "branchLabel": "Bezeichnung", + "branchConditionLabel": "Bedingung", + "branchConditionPlaceholder": "Bedingung eingeben...", + "addEdge": "Verzweigung hinzufügen", + "edgeDefaultName": "Verzweigung", + "noLabel": "Keine Bezeichnung", + "edgeDeletionLastEdge": "Sie können die letzte Verzweigung nicht löschen.", + "edgeDeletionHasOutput": "Eine Verzweigung kann nicht gelöscht werden, wenn sie eine Ausgabe hat." + }, + "periodicForm": { + "intervalLabel": "Intervall", + "intervalHelper": "Wählen Sie aus, wie oft dieser Workflow ausgeführt werden soll", + "everyMinute": "Jede Minute", + "everyHour": "Jede Stunde", + "everyDay": "Jeden Tag", + "everyWeek": "Jede Woche", + "everyMonth": "Jeden Monat", + "hour": "Stunde", + "minute": "Minute", + "dayOfWeek": "Wochentag", + "dayOfMonth": "Tag eines Monats", + "hourPlaceholder": "0-23", + "minutePlaceholder": "0-59", + "dayOfMonthPlaceholder": "1-31", + "minuteHelper": "Dieser Workflow wird jede Minute ausgeführt" } } diff --git a/web-frontend/modules/integrations/locales/fr.json b/web-frontend/modules/integrations/locales/fr.json index 04e8f1858e..def80f4cd4 100644 --- a/web-frontend/modules/integrations/locales/fr.json +++ b/web-frontend/modules/integrations/locales/fr.json @@ -3,7 +3,9 @@ "localBaserowSummary": "Baserow local - {name}, {username}", "localBaserow": "Baserow local", "localBaserowWarning": "Associer votre compte donne à tous ceux qui peuvent modifier l'application un accès complet aux données auxquelles vous avez accès. Il est possible de créer un deuxième utilisateur, donner les bonnes autorisations et utiliser celui-ci.", - "smtp": "Mail SMTP" + "smtp": "Mail SMTP", + "ai": "IA", + "slackBot": "Bot Slack" }, "serviceType": { "localBaserowGetRow": "Obtenir une ligne", @@ -45,7 +47,24 @@ "coreRouterDescription": "Poursuit vers l'une des branches en fonction d'une condition.", "corePeriodic": "Déclencheur périodique", "corePeriodicDescription": "Déclenche le scénario à des intervalles spécifiés", - "corePeriodicErrorIntervalMissing": "Un intervalle est nécessaire." + "corePeriodicErrorIntervalMissing": "Un intervalle est nécessaire.", + "aiAgent": "Prompt pour l'IA", + "aiAgentDescription": "Envoyer des prompts aux modèles d'IA génératifs configurés.", + "errorNoIntegrationSelected": "Aucune intégration sélectionnée", + "errorNoAIProviderSelected": "Aucun service d'IA n'a été sélectionné", + "errorNoAIModelSelected": "Aucun modèle d'IA n'est sélectionné", + "errorNoPromptProvided": "Aucun prompt n'a été fournie", + "errorNoChoicesProvided": "Aucun choix n'est fait pour le type de sortie", + "coreHTTPTrigger": "Recevoir une requête HTTP", + "coreHTTPTriggerDescription": "Déclenché quand une requête HTTP est reçue.", + "coreIteration": "Itérateur", + "coreIterationDescription": "Itère sur une liste d'éléments.", + "errorIterationSourceMissing": "Source manquante", + "slackWriteMessage": "Envoyer un message Slack", + "slackWriteMessageDescription": "Envoie un message à un #canal Slack spécifique", + "slackWriteMessageMissingChannel": "Un canal est nécessaire.", + "slackWriteMessageMissingMessage": "Un message est nécessaire.", + "slackWriteMessageMissingIntegration": "Aucune intégration Slack n'a été sélectionnée." }, "localBaserowForm": { "user": "Utilisateur", @@ -84,7 +103,8 @@ "filterTypeNotFound": "Le type de filtre n'est pas compatible.", "useFormulaForValue": "Utiliser une formule pour ce filtre", "useDefaultForValue": "Utiliser le filtre par défaut pour ce champ", - "formulaFilterInputPlaceholder": "Saisissez un texte..." + "formulaFilterInputPlaceholder": "Saisissez une valeur...", + "textFilterInputPlaceholder": "Saisissez le texte..." }, "localBaserowTableServiceSortForm": { "noSortTitle": "Vous n'avez pas encore créé de tri pour cette source de données", @@ -112,7 +132,7 @@ "localBaserowIntegrationType": { "localBaserowSummary": "Baserow local - {name}, {username}", "localBaserowNoUser": "Baserow local - Non configuré", - "localBaserowWarning": "Associer votre compte donne à tous ceux qui peuvent modifier l'application un accès complet aux données auxquelles vous avez accès. Il est possible de créer un deuxième utilisateur, donner les bonnes autorisations et utiliser celui-ci." + "localBaserowWarning": "Associer votre compte donne à tous ceux qui peuvent modifier l'application un accès complet aux données auxquelles vous avez accès. Il est possible de créer un deuxième utilisateur, lui donner les bonnes autorisations et utiliser celui-ci." }, "integrationsCommon": { "singleRow": "Une ligne", @@ -210,7 +230,7 @@ "periodicForm": { "intervalLabel": "Interval", "intervalHelper": "Choisissez la fréquence d'exécution de ce scénario", - "everyMinute": "Chaque minute", + "everyMinute": "Toutes les {minute} minutes", "everyHour": "Toutes les heures", "everyDay": "Tous les jours", "everyWeek": "Chaque semaine", @@ -222,12 +242,81 @@ "hourPlaceholder": "0-23", "minutePlaceholder": "0-59", "dayOfMonthPlaceholder": "1-31", - "minuteHelper": "Ce scénario sera exécuté toutes les minutes", + "minuteHelper": "Ce scénario sera exécuté toutes les X minutes", "hourHelper": "Ce scénario s'exécutera toutes les heures à la minute spécifiée dans votre fuseau horaire local ({timezone})", "dayHelper": "Ce scénario sera exécuté chaque jour à l'heure spécifiée dans votre fuseau horaire local ({timezone})", "weekHelper": "Ce scénario sera exécuté chaque semaine au jour et à l'heure spécifiés dans votre fuseau horaire local ({timezone})", "monthHelper": "Ce scénario sera exécuté chaque mois le jour et l'heure spécifiés dans votre fuseau horaire local ({timezone})", "deactivatedTitle": "Déclenchement périodique désactivé", - "deactivatedText": "Ce déclencheur périodique a été automatiquement désactivé en raison d'échecs consécutifs." + "deactivatedText": "Ce déclencheur périodique a été automatiquement désactivé en raison d'échecs consécutifs.", + "everyMinuteDefault": "Toutes les X minutes", + "minuteFrequency": "Toutes les minutes", + "minuteFrequencyPlaceholder": "15" + }, + "aiIntegrationType": { + "overridingProviders": "{count} service configuré|{count} services configurés", + "inheritingWorkspace": "Héritage des paramètres d'IA du projet" + }, + "aiForm": { + "description": "Configurer les paramètres du service d'IA pour cette intégration. Par défaut, les paramètres d'IA du projet sont hérités.", + "workspaceSettingsTitle": "Paramètres de configuration d'IA du projet", + "workspaceSettingsDescription": "Cette intégration hérite par défaut des configurations d'IA de votre Projet. Vous pouvez remplacer des services spécifiques ci-dessous.", + "overrideWorkspaceSettings": "Remplacer les paramètres du projet pour ce service", + "inherited": "Héritée", + "overridden": "Remplacé" + }, + "aiAgentServiceForm": { + "integrationLabel": "Intégration", + "providerLabel": "Service d'IA", + "providerPlaceholder": "Sélectionnez un service d'IA...", + "modelLabel": "Modèle IA", + "modelPlaceholder": "Sélectionnez un modèle...", + "outputTypeLabel": "Type de sortie", + "outputTypeHelp": "Choisissez la manière dont l'IA doit formuler sa réponse. Si vous définissez des choix, vous obligez le modèle à ne répondre que par l'un de ces choix.", + "outputTypeText": "Texte", + "outputTypeChoice": "Choix", + "temperatureLabel": "Température", + "temperaturePlaceholder": "Ex : 0.7", + "temperatureHelp": "Contrôle le caractère aléatoire. Les valeurs faibles (0-0,3) sont plus ciblées et déterministes. Les valeurs plus élevées (0,7-2,0) sont plus créatives et variées.", + "promptLabel": "Prompt", + "promptPlaceholder": "Saisissez votre prompt ici...", + "choicesLabel": "Choix", + "choicePlaceholder": "Saisissez un choix...", + "addChoice": "Ajouter un choix", + "choicesRequired": "Au moins un choix est requis" + }, + "coreIterationServiceForm": { + "source": "Source", + "sourcePlaceholder": "Choisissez une source..." + }, + "slackBotIntegrationType": { + "slackBotSummary": "Bot Slack", + "slackBotNoToken": "Bot Slack - Non configuré" + }, + "slackBotForm": { + "tokenLabel": "Token utilisateur pour le bot", + "tokenPlaceholder": "xoxb-1234-...", + "tokenMustStartWith": "Le jeton doit commencer par \"xoxb-\"", + "supportHeading": "Besoin d'aide ?", + "supportDescription": "Si vous avez besoin d'aide pour la connexion avec votre application Slack, veuillez suivre les étapes ci-dessous.", + "supportSetupHeading": "1. Mettre en place l'application", + "supportSetupDescription": "En fonction des paramètres de votre espace de travail Slack, vous pourrez peut-être créer une nouvelle application Slack. Sinon, un administrateur devra le faire pour vous. Si vous réutilisez une application existante qui peut écrire des messages, passez à la section intitulée \"Connexion avec votre application Slack\".", + "supportSetupStep1": "Accédez à la page application de votre espace de travail Slack.", + "supportSetupStep2": "Créez une nouvelle application, choisissez \"A partir de zéro\" et saisissez un nom. Sélectionnez l'espace de travail dans lequel votre application doit fonctionner et cliquez sur \"Créer\".", + "supportSetupStep3": "Dans la barre latérale gauche, naviguez vers \"OAuth & Permissions\", descendez jusqu'à \"Scopes\" et sous \"Bot Token Scopes\", sélectionnez \"Add an OAuth Scope\".", + "supportSetupStep4": "Pour permettre à votre application de poster des messages, ajoutez la portée
chat:write
.", + "supportPairingHeading": "2. Association avec votre application Slack", + "supportPairingStep1": "Si votre application est nouvelle : visitez la page 'Paramètres' > 'Installer application'. Cliquez sur le bouton vert pour installer l'application dans votre Projet.", + "supportPairingStep2": "Copiez votre 'Bot User OAuth Token' et stockez-le dans le champ 'Jeton utilisateur Bot' dans ce formulaire.", + "supportPairingStep3": "Enfin, si votre application est nouvelle : dans Slack, invitez votre application dans le canal choisi avec
/invite @votreAppName votreChannel
" + }, + "slackWriteMessageServiceForm": { + "alertMessage": "Cette action doit être associée à une application Slack. Suivez le guide dans la fenêtre de configuration de l'intégration pour procéder.", + "integrationLabel": "Intégration", + "channelLabel": "Canal", + "channelPlaceholder": "Entrez un nom de canal", + "messageLabel": "Message", + "messagePlaceholder": "Saisissez un message...", + "channelNoPrefix": "Retirer le '#' avant le nom du canal." } } diff --git a/web-frontend/modules/integrations/locales/ko.json b/web-frontend/modules/integrations/locales/ko.json index e3dd59962d..3fe764b5cb 100644 --- a/web-frontend/modules/integrations/locales/ko.json +++ b/web-frontend/modules/integrations/locales/ko.json @@ -5,12 +5,14 @@ }, "integrationType": { "localBaserow": "로컬 Baserow", - "smtp": "SMTP 이메일" + "smtp": "SMTP 이메일", + "slackBot": "슬랙 봇", + "ai": "AI" }, "localBaserowIntegrationType": { "localBaserowSummary": "로컬 Baserow - {name}, {username}", "localBaserowNoUser": "로컬 Baserow - 구성되지 않음", - "localBaserowWarning": "귀하의 계정을 승인하면 애플리케이션에 편집 권한이 있는 모든 사용자가 귀하가 접근할 수 있는 데이터에 완전한 접근 권한을 갖게 됩니다. 두 번째 사용자를 생성하여 적절한 권한을 부여하고 그 사용자를 사용할 수 있습니다." + "localBaserowWarning": "계정을 승인하면 애플리케이션 편집 권한이 있는 모든 사용자에게 귀하가 접근하는 데이터에 대한 전체 접근 권한이 부여됩니다. 두 번째 사용자를 생성하고 적절한 권한을 부여하여 해당 사용자를 사용할 수 있습니다." }, "serviceType": { "localBaserowGetRow": "단일 행 가져오기", @@ -52,14 +54,31 @@ "coreRouterDescription": "조건에 따라 워크플로를 다음 브랜치로 라우팅합니다.", "corePeriodic": "주기적 트리거", "corePeriodicDescription": "지정된 간격으로 주기적으로 워크플로우를 트리거합니다", - "corePeriodicErrorIntervalMissing": "간격이 필요합니다." + "corePeriodicErrorIntervalMissing": "간격이 필요합니다.", + "coreHTTPTrigger": "HTTP 요청 수신", + "coreHTTPTriggerDescription": "HTTP 요청이 수신되었을 때 트리거됩니다.", + "coreIteration": "반복자", + "coreIterationDescription": "항목을 반복합니다.", + "errorIterationSourceMissing": "소스 속성이 누락되었습니다", + "aiAgent": "AI 프롬프트", + "aiAgentDescription": "구성된 생성 AI 모델을 사용하여 AI 프롬프트를 실행합니다.", + "errorNoIntegrationSelected": "통합이 선택되지 않았습니다", + "errorNoAIProviderSelected": "AI 공급자가 선택되지 않았습니다", + "errorNoAIModelSelected": "AI 모델이 선택되지 않았습니다", + "errorNoPromptProvided": "프롬프트가 제공되지 않음", + "errorNoChoicesProvided": "선택 출력 유형에 대한 선택 사항이 제공되지 않았습니다", + "slackWriteMessage": "Slack 메시지 보내기", + "slackWriteMessageDescription": "특정 Slack 채널에 메시지를 보냅니다", + "slackWriteMessageMissingChannel": "채널이 필요합니다.", + "slackWriteMessageMissingMessage": "메시지가 필요합니다.", + "slackWriteMessageMissingIntegration": "Slack 통합이 선택되지 않았습니다." }, "userSourceType": { "localBaserow": "Baserow 테이블 인증" }, "localBaserowForm": { "user": "사용자", - "userMessage": "이 연결을 생성함으로써, 귀하는 애플리케이션이 귀하의 계정을 사용하여 로컬 Baserow 작업공간에서 변경을 할 수 있도록 승인하는 것입니다." + "userMessage": "이 연결을 생성하면 애플리케이션이 귀하의 계정을 사용하여 로컬 Baserow 작업 공간에서 변경 작업을 수행할 수 있도록 승인하게 됩니다." }, "localBaserowGetRowForm": { "rowFieldLabel": "행 ID", @@ -100,9 +119,10 @@ "filterTypeNotFound": "필터 유형이 호환되지 않습니다.", "noCompatibleFilterTypesErrorTitle": "호환 가능한 필터 유형 없음", "noCompatibleFilterTypesErrorMessage": "필드 중 호환 가능한 필터 유형이 없습니다", - "formulaFilterInputPlaceholder": "텍스트 입력...", + "formulaFilterInputPlaceholder": "수식을 선택하세요...", "useFormulaForValue": "이 필터에 수식 사용", - "useDefaultForValue": "이 필드에 기본 필터 사용" + "useDefaultForValue": "이 필드에 기본 필터 사용", + "textFilterInputPlaceholder": "텍스트를 입력하세요..." }, "localBaserowTableServiceSortForm": { "noSortTitle": "아직 데이터 소스 정렬을 생성하지 않았습니다", @@ -208,7 +228,7 @@ "periodicForm": { "intervalLabel": "간격", "intervalHelper": "이 워크플로를 얼마나 자주 실행할지 선택하세요", - "everyMinute": "매 분", + "everyMinute": "매 {minute} 분마다", "everyHour": "매 시간", "everyDay": "매 일", "everyWeek": "매 주", @@ -220,12 +240,81 @@ "hourPlaceholder": "0-23", "minutePlaceholder": "0-59", "dayOfMonthPlaceholder": "1-31", - "minuteHelper": "이 워크플로는 매분마다 실행됩니다", + "minuteHelper": "이 워크플로는 지정된 분마다 실행됩니다", "hourHelper": "이 워크플로는 현지 시간대({timezone})에서 지정된 분마다 실행됩니다", "dayHelper": "이 워크플로는 매일 현지 시간대({timezone})의 지정된 시간에 실행됩니다", "weekHelper": "이 워크플로는 매주 지정된 날짜와 시간에 현지 시간대({timezone})로 실행됩니다", "monthHelper": "이 워크플로는 매월 지정된 날짜와 시간에 현지 시간대({timezone})로 실행됩니다", "deactivatedTitle": "주기적 트리거 비활성화됨", - "deactivatedText": "연속적인 실패로 인해 이 주기적 트리거가 자동으로 비활성화되었습니다." + "deactivatedText": "연속적인 실패로 인해 이 주기적 트리거가 자동으로 비활성화되었습니다.", + "everyMinuteDefault": "지정된 분마다", + "minuteFrequency": "매 분마다", + "minuteFrequencyPlaceholder": "15" + }, + "aiIntegrationType": { + "inheritingWorkspace": "작업 공간 AI 설정 상속", + "overridingProviders": "{count}개의 공급자 재정의 중|{count}개의 공급자 재정의 중" + }, + "slackBotIntegrationType": { + "slackBotSummary": "슬랙 봇", + "slackBotNoToken": "슬랙 봇 - 구성되지 않음" + }, + "aiForm": { + "description": "이 통합에 대한 AI 공급자 설정을 구성하세요. 기본적으로 Workspace AI 설정은 상속됩니다.", + "workspaceSettingsTitle": "Workspace AI 설정", + "workspaceSettingsDescription": "이 통합은 기본적으로 작업 공간의 AI 공급자 설정을 상속합니다. 아래에서 특정 공급자를 재정의할 수 있습니다.", + "overrideWorkspaceSettings": "이 공급자에 대한 작업 공간 설정을 재정의합니다", + "inherited": "상속받음", + "overridden": "재정의됨" + }, + "aiAgentServiceForm": { + "integrationLabel": "통합", + "providerLabel": "AI 공급업체", + "providerPlaceholder": "AI 공급업체를 선택하세요...", + "modelLabel": "AI 모델", + "modelPlaceholder": "모델을 선택하세요...", + "outputTypeLabel": "출력 유형", + "outputTypeHelp": "AI가 응답 형식을 어떻게 구성해야 할지 선택하세요. 선택지를 정의하면 모델이 해당 선택지 중 하나만으로 응답하도록 강제합니다.", + "outputTypeText": "텍스트", + "outputTypeChoice": "선택", + "temperatureLabel": "온도", + "temperaturePlaceholder": "예) 0.7", + "temperatureHelp": "무작위성을 조절합니다. 값이 낮을수록(0–0.3) 더 집중되고 결정적인 결과를 내며, 값이 높을수록(0.7–2.0) 더 창의적이고 다양하게 출력됩니다.", + "promptLabel": "프롬프트", + "promptPlaceholder": "여기에 프롬프트를 입력하세요...", + "choicesLabel": "선택", + "choicePlaceholder": "선택 옵션을 입력하세요...", + "addChoice": "선택 추가", + "choicesRequired": "최소한 하나의 선택이 필요합니다" + }, + "slackBotForm": { + "tokenLabel": "봇 사용자 토큰", + "tokenPlaceholder": "xoxb-1234-...", + "tokenMustStartWith": "토큰은 \"xoxb-\"로 시작해야 합니다", + "supportHeading": "도움이 필요하신가요?", + "supportDescription": "Slack 앱과 페어링하는 데 도움이 필요하면 아래 단계를 참조하세요.", + "supportSetupHeading": "1. 앱 설정", + "supportSetupDescription": "Slack 작업 공간 설정에 따라 새 Slack 앱을 생성할 수 있습니다. 그렇지 않은 경우 관리자가 대신 생성해야 합니다. 메시지를 작성할 수 있는 기존 앱을 재사용하는 경우 'Slack 앱과 페어링하기' 섹션으로 건너뛰세요.", + "supportSetupStep1": "워크스페이스의 앱 페이지로 이동하세요.", + "supportSetupStep2": "새 앱을 만들고 '처음부터'를 선택한 후 이름을 입력하세요. 앱이 작동할 작업 공간을 선택하고 '만들기'를 클릭하세요.", + "supportSetupStep3": "왼쪽 사이드바에서 'OAuth 및 권한'으로 이동한 후 범위까지 아래로 스크롤하여 '봇 토큰 범위' 아래에서 'OAuth 범위 추가'를 선택합니다.", + "supportSetupStep4": "앱에서 메시지를 게시할 수 있도록 하려면
chat:write
범위를 추가하세요.", + "supportPairingHeading": "2. Slack 앱과 페어링", + "supportPairingStep1": "앱이 새로 설치된 경우: '설정' > '앱 설치'로 이동하세요. 녹색 버튼을 클릭하여 작업 공간에 앱을 설치하세요.", + "supportPairingStep2": "'봇 사용자 OAuth 토큰'을 복사하여 이 양식의 '봇 사용자 토큰' 필드에 저장하세요.", + "supportPairingStep3": "마지막으로, 앱이 새롭다면 Slack에서
/invite @yourAppName yourChannel
을 사용하여 선택한 채널에 앱을 초대하세요" + }, + "slackWriteMessageServiceForm": { + "alertMessage": "이 작업은 Slack 앱과 연동되어야 합니다. 시작하려면 통합 팝업의 가이드를 따르세요.", + "integrationLabel": "통합", + "channelLabel": "채널", + "channelPlaceholder": "채널 이름을 입력하세요", + "messageLabel": "메시지", + "messagePlaceholder": "메시지를 입력하세요...", + "channelNoPrefix": "채널 이름 앞에 있는 '#'을 제거하세요." + }, + "coreIterationServiceForm": { + "source": "소스", + "sourcePlaceholder": "출처를 선택하세요..." } } diff --git a/web-frontend/modules/integrations/locales/nl.json b/web-frontend/modules/integrations/locales/nl.json index 55f5ad0ca3..04ef0e796b 100644 --- a/web-frontend/modules/integrations/locales/nl.json +++ b/web-frontend/modules/integrations/locales/nl.json @@ -32,7 +32,8 @@ "noCompatibleFilterTypesErrorMessage": "Geen van uw velden heeft passende filtertypen", "formulaFilterInputPlaceholder": "Tekst invoeren...", "useFormulaForValue": "Gebruik een formule voor dit filter", - "useDefaultForValue": "Gebruik het standaardfilter voor dit veld" + "useDefaultForValue": "Gebruik het standaardfilter voor dit veld", + "textFilterInputPlaceholder": "Tekst invoeren..." }, "localBaserowTableSelector": { "databaseFieldLabel": "Database", @@ -45,7 +46,9 @@ "localBaserow": "Lokaal Baserow", "localBaserowSummary": "Lokaal Baserow - {name}, {username}", "localBaserowWarning": "Door jouw account te autoriseren krijgt iedereen die bewerkingsrechten heeft voor de applicatie volledige toegang tot de gegevens waar jij toegang tot hebt. Het is mogelijk om een tweede gebruiker aan te maken, de juiste rechten te geven en die te gebruiken.", - "smtp": "SMTP e-mail" + "smtp": "SMTP e-mail", + "ai": "AI", + "slackBot": "Slack Bot" }, "serviceType": { "localBaserowGetRow": "Rij ophalen", @@ -87,7 +90,24 @@ "coreRouterDescription": "Leidt de workflow naar de volgende vertakking op basis van een voorwaarde.", "corePeriodic": "Periodieke trigger", "corePeriodicDescription": "Triggert de workflow op periodieke basis met gespecificeerde intervallen", - "corePeriodicErrorIntervalMissing": "Een interval is vereist." + "corePeriodicErrorIntervalMissing": "Een interval is vereist.", + "coreHTTPTrigger": "Een HTTP-verzoek ontvangen", + "coreHTTPTriggerDescription": "Wordt geactiveerd wanneer een HTTP-verzoek wordt ontvangen.", + "coreIteration": "Iterator", + "coreIterationDescription": "Itereren op items.", + "errorIterationSourceMissing": "Ontbrekende broneigenschap", + "aiAgent": "AI prompt", + "aiAgentDescription": "AI prompt uitvoeren met behulp van geconfigureerde generatieve AI-modellen.", + "errorNoIntegrationSelected": "Geen integratie geselecteerd", + "errorNoAIProviderSelected": "Geen AI-aanbieder geselecteerd", + "errorNoAIModelSelected": "Geen AI-model geselecteerd", + "errorNoPromptProvided": "Geen prompt voorzien", + "errorNoChoicesProvided": "Geen keuzes voor uitvoer type", + "slackWriteMessage": "Stuur een Slack-bericht", + "slackWriteMessageDescription": "Stuurt een bericht naar een specifiek Slack #kanaal", + "slackWriteMessageMissingChannel": "Een kanaal is vereist.", + "slackWriteMessageMissingMessage": "Een bericht is vereist.", + "slackWriteMessageMissingIntegration": "Geen Slack-integratie geselecteerd." }, "userSourceType": { "localBaserow": "Baserow tabel authenticatie" @@ -228,6 +248,75 @@ "weekHelper": "Deze workflow wordt elke week uitgevoerd op de opgegeven dag en tijd in je lokale tijdzone ({timezone})", "monthHelper": "Deze workflow wordt elke maand uitgevoerd op de opgegeven dag en tijd in je lokale tijdzone ({timezone}).", "deactivatedTitle": "Periodieke trigger gedeactiveerd", - "deactivatedText": "Deze periodieke trigger is automatisch gedeactiveerd vanwege opeenvolgende fouten." + "deactivatedText": "Deze periodieke trigger is automatisch gedeactiveerd vanwege opeenvolgende fouten.", + "everyMinuteDefault": "Elke opgegeven minuut", + "minuteFrequency": "Elke minuut", + "minuteFrequencyPlaceholder": "15" + }, + "aiIntegrationType": { + "inheritingWorkspace": "Werkruimte AI-instellingen erven", + "overridingProviders": "Opheffen van {count} provider|Overschrijving {count} providers" + }, + "aiForm": { + "description": "Configureer AI-providerinstellingen voor deze integratie. Standaard worden de AI-instellingen voor de werkruimte geërfd.", + "workspaceSettingsTitle": "Werkruimte AI-instellingen", + "workspaceSettingsDescription": "Deze integratie erft standaard AI-providerinstellingen van je werkruimte. Hieronder kun je specifieke providers overschrijven.", + "overrideWorkspaceSettings": "Werkruimte-instellingen voor deze provider overschrijven", + "inherited": "Geërfd", + "overridden": "Overschreven" + }, + "aiAgentServiceForm": { + "integrationLabel": "Integratie", + "providerLabel": "AI-aanbieder", + "providerPlaceholder": "Selecteer een AI-provider...", + "modelLabel": "AI-model", + "modelPlaceholder": "Selecteer een model...", + "outputTypeLabel": "Uitvoer type", + "outputTypeHelp": "Kies hoe de AI moet reageren. Als je keuzes definieert, dwing je het model om alleen te reageren met een van die keuzes.", + "outputTypeText": "Tekst", + "outputTypeChoice": "Keuze", + "temperatureLabel": "Temperatuur", + "temperaturePlaceholder": "e.g. 0.7", + "temperatureHelp": "Regelt willekeur. Lagere waarden (0-0,3) zijn meer gefocust en deterministisch. Hogere waarden (0,7-2,0) zijn creatiever en gevarieerder.", + "promptLabel": "Prompt", + "promptPlaceholder": "Voer hier je vraag in...", + "choicesLabel": "Keuzes", + "choicePlaceholder": "Voer een keuzeoptie in...", + "addChoice": "Keuze toevoegen", + "choicesRequired": "Minstens één keuze is vereist" + }, + "coreIterationServiceForm": { + "source": "Bron", + "sourcePlaceholder": "Kies een bron..." + }, + "slackBotIntegrationType": { + "slackBotSummary": "Slack Bot", + "slackBotNoToken": "Slack Bot - Niet geconfigureerd" + }, + "slackBotForm": { + "tokenLabel": "Bot gebruiker token", + "tokenPlaceholder": "xoxb-1234-...", + "tokenMustStartWith": "Token moet beginnen met \"xoxb-\".", + "supportHeading": "Hulp nodig?", + "supportDescription": "Als je hulp nodig hebt bij het koppelen met je Slack-app, raadpleeg dan de onderstaande stappen.", + "supportSetupHeading": "1. De app instellen", + "supportSetupDescription": "Afhankelijk van de instellingen van je Slack-werkruimte, kun je misschien een nieuwe Slack-app maken. Anders moet een beheerder dit voor je doen. Als je een bestaande app hergebruikt die berichten kan schrijven, ga dan naar het gedeelte 'Koppelen met je Slack app'.", + "supportSetupStep1": "Navigeer naar de apps pagina van je werkruimte.", + "supportSetupStep2": "Creëer een nieuwe app, kies 'Vanuit niks' en voer een naam in. Selecteer de werkruimte waarin je app moet werken, en klik 'Maken'.", + "supportSetupStep3": "Navigeer in de linker zijbalk naar 'OAuth & Toestemmingen', scroll naar beneden naar Scopes en selecteer onder 'Bot Token Scopes' 'OAuth scope toevoegen'.", + "supportSetupStep4": "Om je app berichten te laten posten, voeg je de scope
chat:write
toe.", + "supportPairingHeading": "2. Koppelen met je Slack-app", + "supportPairingStep1": "Als je app nieuw is: ga naar 'Instellingen' > 'App installeren'. Klik op de groene knop om de app op je werkruimte te installeren.", + "supportPairingStep2": "Kopieer uw 'Bot Gebruiker OAuth Token' en sla deze op in het veld 'Bot Gebruiker Token' in dit formulier.", + "supportPairingStep3": "Tot slot, als je app nieuw is: nodig in Slack je app uit voor het door jou gekozen kanaal met
/invite @yourAppName yourChannel
" + }, + "slackWriteMessageServiceForm": { + "alertMessage": "Deze actie moet worden gekoppeld met een Slack-app. Volg de handleiding in de integratie popup om aan de slag te gaan.", + "integrationLabel": "Integratie", + "channelLabel": "Kanaal", + "channelPlaceholder": "Een kanaalnaam invoeren", + "messageLabel": "Bericht", + "messagePlaceholder": "Voer een bericht in...", + "channelNoPrefix": "Verwijder de '#' voor de kanaalnaam." } } From 2466bf02446d4af05ce382f642fe4eac94185801 Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:02:58 +0100 Subject: [PATCH 6/6] Removed database FF, added changelog entries (#4275) --- backend/src/baserow/api/search/urls.py | 18 ++++------- .../contrib/database/api/field_rules/urls.py | 32 ++++++++----------- .../contrib/database/field_rules/handlers.py | 3 -- backend/src/baserow/core/feature_flags.py | 2 -- .../feature/3826_workspace_search.json | 9 ++++++ .../3829_date_dependencies_in_table.json | 9 ++++++ .../dateDependency/DateDependencyMenuItem.vue | 6 +--- .../baserow_enterprise/dateDependencyTypes.js | 2 -- .../modules/baserow_enterprise/plugin.js | 16 +++------- .../core/components/sidebar/SidebarMenu.vue | 7 ---- web-frontend/modules/core/layouts/app.vue | 19 ++--------- .../modules/core/plugins/featureFlags.js | 2 -- web-frontend/modules/database/viewTypes.js | 3 -- 13 files changed, 48 insertions(+), 80 deletions(-) create mode 100644 changelog/entries/unreleased/feature/3826_workspace_search.json create mode 100644 changelog/entries/unreleased/feature/3829_date_dependencies_in_table.json diff --git a/backend/src/baserow/api/search/urls.py b/backend/src/baserow/api/search/urls.py index a218b71064..cb24d54b1f 100644 --- a/backend/src/baserow/api/search/urls.py +++ b/backend/src/baserow/api/search/urls.py @@ -1,17 +1,13 @@ from django.urls import path from baserow.api.search.views import WorkspaceSearchView -from baserow.core.feature_flags import FF_WORKSPACE_SEARCH, feature_flag_is_enabled app_name = "baserow.api.search" -urlpatterns = [] - -if feature_flag_is_enabled(FF_WORKSPACE_SEARCH): - urlpatterns = [ - path( - "workspace//", - WorkspaceSearchView.as_view(), - name="workspace_search", - ), - ] +urlpatterns = [ + path( + "workspace//", + WorkspaceSearchView.as_view(), + name="workspace_search", + ), +] diff --git a/backend/src/baserow/contrib/database/api/field_rules/urls.py b/backend/src/baserow/contrib/database/api/field_rules/urls.py index 06651968b4..d8ed5f2b8f 100644 --- a/backend/src/baserow/contrib/database/api/field_rules/urls.py +++ b/backend/src/baserow/contrib/database/api/field_rules/urls.py @@ -1,24 +1,20 @@ from django.urls import re_path -from baserow.core.feature_flags import FF_DATE_DEPENDENCY, feature_flag_is_enabled +from .views import FieldRulesView, FieldRuleView, InvalidRowsView app_name = "baserow.contrib.database.api.field_rules" -urlpatterns = [] -if feature_flag_is_enabled(FF_DATE_DEPENDENCY): - from .views import FieldRulesView, FieldRuleView, InvalidRowsView - - urlpatterns += [ - re_path(r"^(?P[0-9]+)/$", FieldRulesView.as_view(), name="list"), - re_path( - r"^(?P[0-9]+)/rule/(?P[0-9]+)/$", - FieldRuleView.as_view(), - name="item", - ), - re_path( - r"^(?P[0-9]+)/invalid-rows/$", - InvalidRowsView.as_view(), - name="invalid_rows", - ), - ] +urlpatterns = [ + re_path(r"^(?P[0-9]+)/$", FieldRulesView.as_view(), name="list"), + re_path( + r"^(?P[0-9]+)/rule/(?P[0-9]+)/$", + FieldRuleView.as_view(), + name="item", + ), + re_path( + r"^(?P[0-9]+)/invalid-rows/$", + InvalidRowsView.as_view(), + name="invalid_rows", + ), +] diff --git a/backend/src/baserow/contrib/database/field_rules/handlers.py b/backend/src/baserow/contrib/database/field_rules/handlers.py index 2fa5e9f10f..1ef1917454 100644 --- a/backend/src/baserow/contrib/database/field_rules/handlers.py +++ b/backend/src/baserow/contrib/database/field_rules/handlers.py @@ -15,7 +15,6 @@ from baserow.contrib.database.table.cache import clear_generated_model_cache from baserow.contrib.database.table.models import GeneratedTableModel, Table from baserow.core.db import specific_iterator -from baserow.core.feature_flags import FF_DATE_DEPENDENCY, feature_flag_is_enabled from .collector import FieldRuleCollector from .exceptions import FieldRuleTableMismatch, NoRuleError @@ -50,8 +49,6 @@ def has_field_rules(self) -> bool: Returns `True` if the table contains active field rules. """ - if not feature_flag_is_enabled(FF_DATE_DEPENDENCY): - return False if not self.table.field_rules_validity_column_added: return False return bool(self.applicable_rules_with_types) diff --git a/backend/src/baserow/core/feature_flags.py b/backend/src/baserow/core/feature_flags.py index a1f79bbd80..3650627218 100644 --- a/backend/src/baserow/core/feature_flags.py +++ b/backend/src/baserow/core/feature_flags.py @@ -2,8 +2,6 @@ from baserow.core.exceptions import FeatureDisabledException -FF_WORKSPACE_SEARCH = "workspace-search" -FF_DATE_DEPENDENCY = "date_dependency" FF_ENABLE_ALL = "*" diff --git a/changelog/entries/unreleased/feature/3826_workspace_search.json b/changelog/entries/unreleased/feature/3826_workspace_search.json new file mode 100644 index 0000000000..b9d7cb3e8b --- /dev/null +++ b/changelog/entries/unreleased/feature/3826_workspace_search.json @@ -0,0 +1,9 @@ +{ + "type": "feature", + "message": "Workspace search", + "issue_origin": "github", + "issue_number": 3826, + "domain": "database", + "bullet_points": [], + "created_at": "2025-11-17" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/feature/3829_date_dependencies_in_table.json b/changelog/entries/unreleased/feature/3829_date_dependencies_in_table.json new file mode 100644 index 0000000000..bf0b3f2a11 --- /dev/null +++ b/changelog/entries/unreleased/feature/3829_date_dependencies_in_table.json @@ -0,0 +1,9 @@ +{ + "type": "feature", + "message": "Date dependencies in table", + "issue_origin": "github", + "issue_number": 3829, + "domain": "database", + "bullet_points": [], + "created_at": "2025-11-17" +} \ No newline at end of file diff --git a/enterprise/web-frontend/modules/baserow_enterprise/components/dateDependency/DateDependencyMenuItem.vue b/enterprise/web-frontend/modules/baserow_enterprise/components/dateDependency/DateDependencyMenuItem.vue index b563462b27..c4ba42c660 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/components/dateDependency/DateDependencyMenuItem.vue +++ b/enterprise/web-frontend/modules/baserow_enterprise/components/dateDependency/DateDependencyMenuItem.vue @@ -1,6 +1,6 @@