diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0bb3eadc539..949aa57ae6a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,38 +1,38 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "name": "elizaos-dev", - "dockerFile": "Dockerfile", - "build": { - "args": { - "NODE_VER": "23.5.0", - "BUN_VER": "1.2.2" - } - }, - "privileged": true, - "runArgs": [ - "-p=3000:3000", // Add port for server api - "-p=5173:5173", // Add port for client - //"--volume=/usr/lib/wsl:/usr/lib/wsl", // uncomment for WSL - //"--volume=/mnt/wslg:/mnt/wslg", // uncomment for WSL - "--gpus=all", // ! uncomment for vGPU - //"--device=/dev/dxg", // uncomment this for vGPU under WSL - "--device=/dev/dri" - ], - "containerEnv": { - //"MESA_D3D12_DEFAULT_ADAPTER_NAME": "NVIDIA", // uncomment for WSL - //"LD_LIBRARY_PATH": "/usr/lib/wsl/lib" // uncomment for WSL - }, - "customizations": { - "vscode": { - "extensions": [ - "vscode.json-language-features", - "vscode.css-language-features", - // "foxundermoon.shell-format", - // "dbaeumer.vscode-eslint", - // "esbenp.prettier-vscode" - "ms-python.python" - ] - } - }, - "features": {} -} \ No newline at end of file + "name": "elizaos-dev", + "dockerFile": "Dockerfile", + "build": { + "args": { + "NODE_VER": "23.5.0", + "BUN_VER": "1.2.2" + } + }, + "privileged": true, + "runArgs": [ + "-p=3000:3000", // Add port for server api + "-p=5173:5173", // Add port for client + //"--volume=/usr/lib/wsl:/usr/lib/wsl", // uncomment for WSL + //"--volume=/mnt/wslg:/mnt/wslg", // uncomment for WSL + "--gpus=all", // ! uncomment for vGPU + //"--device=/dev/dxg", // uncomment this for vGPU under WSL + "--device=/dev/dri" + ], + "containerEnv": { + //"MESA_D3D12_DEFAULT_ADAPTER_NAME": "NVIDIA", // uncomment for WSL + //"LD_LIBRARY_PATH": "/usr/lib/wsl/lib" // uncomment for WSL + }, + "customizations": { + "vscode": { + "extensions": [ + "vscode.json-language-features", + "vscode.css-language-features", + // "foxundermoon.shell-format", + // "dbaeumer.vscode-eslint", + // "esbenp.prettier-vscode" + "ms-python.python" + ] + } + }, + "features": {} +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d39655b5274..8f491e12f8d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,48 +1,48 @@ name: ci on: - push: - branches: [main] - pull_request: - branches: [main] + push: + branches: [main] + pull_request: + branches: [main] jobs: - check: - runs-on: ubuntu-latest - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - TURBO_REMOTE_ONLY: true - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - - uses: actions/setup-node@v4 - with: - node-version: "23" - - - name: Install dependencies - run: bun install - - - name: Setup Biome CLI - uses: biomejs/setup-biome@v2 - with: - version: latest - - - name: Run Biome - run: biome ci - - - name: Create test env file - run: | - echo "TEST_DATABASE_CLIENT=sqlite" > packages/core/.env.test - echo "NODE_ENV=test" >> packages/core/.env.test - - - name: Run tests - run: cd packages/core && bun test:coverage - - - name: Build packages - run: bun run build - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} + check: + runs-on: ubuntu-latest + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + TURBO_REMOTE_ONLY: true + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + + - uses: actions/setup-node@v4 + with: + node-version: "23" + + - name: Install dependencies + run: bun install + + - name: Setup Biome CLI + uses: biomejs/setup-biome@v2 + with: + version: latest + + - name: Run Biome + run: biome ci + + - name: Create test env file + run: | + echo "TEST_DATABASE_CLIENT=sqlite" > packages/core/.env.test + echo "NODE_ENV=test" >> packages/core/.env.test + + - name: Run tests + run: cd packages/core && bun test:coverage + + - name: Build packages + run: bun run build + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 359ff9d6780..6bfc55477ee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,55 +1,55 @@ name: "CodeQL Advanced" on: - push: - branches: ["main", "develop"] - pull_request: - branches: ["main", "develop"] - schedule: - - cron: "29 8 * * 6" + push: + branches: ["main", "develop"] + pull_request: + branches: ["main", "develop"] + schedule: + - cron: "29 8 * * 6" jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index 5b2cd3598c6..bddf628627b 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -1,30 +1,30 @@ name: Generate Changelog on: - push: - tags: - - "*" + push: + tags: + - "*" jobs: - changelog: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - ref: main - token: ${{ secrets.CHANGELOG_GITHUB_TOKEN }} - - name: Generate Changelog - run: | - export PATH="$PATH:/home/runner/.local/share/gem/ruby/3.0.0/bin" - gem install --user-install github_changelog_generator - github_changelog_generator \ - -u ${{ github.repository_owner }} \ - -p ${{ github.event.repository.name }} \ - --token ${{ secrets.CHANGELOG_GITHUB_TOKEN }} - - name: Commit Changelog - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: update changelog" - branch: main - file_pattern: "CHANGELOG.md" - commit_author: "GitHub Action " + changelog: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.CHANGELOG_GITHUB_TOKEN }} + - name: Generate Changelog + run: | + export PATH="$PATH:/home/runner/.local/share/gem/ruby/3.0.0/bin" + gem install --user-install github_changelog_generator + github_changelog_generator \ + -u ${{ github.repository_owner }} \ + -p ${{ github.event.repository.name }} \ + --token ${{ secrets.CHANGELOG_GITHUB_TOKEN }} + - name: Commit Changelog + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update changelog" + branch: main + file_pattern: "CHANGELOG.md" + commit_author: "GitHub Action " diff --git a/.github/workflows/generate-readme-translations.yml b/.github/workflows/generate-readme-translations.yml index 5e142c2980d..66587a865c1 100644 --- a/.github/workflows/generate-readme-translations.yml +++ b/.github/workflows/generate-readme-translations.yml @@ -1,98 +1,98 @@ name: Generate Readme Translations on: - push: - branches: - - "1222--README-ci-auto-translation" + push: + branches: + - "1222--README-ci-auto-translation" jobs: - translation: - runs-on: ubuntu-latest - strategy: - matrix: - language: - [ - { code: "CN", name: "Chinese" }, - { code: "DE", name: "German" }, - { code: "ES", name: "Spanish" }, - { code: "FR", name: "French" }, - { code: "HE", name: "Hebrew" }, - { code: "IT", name: "Italian" }, - { code: "JA", name: "Japanese" }, - { code: "KOR", name: "Korean" }, - { code: "PTBR", name: "Portuguese (Brazil)" }, - { code: "RU", name: "Russian" }, - { code: "TH", name: "Thai" }, - { code: "TR", name: "Turkish" }, - { code: "VI", name: "Vietnamese" }, - { code: "AR", name: "Arabic" }, - { code: "RS", name: "Srpski" }, - { code: "TG", name: "Tagalog" }, - { code: "PL", name: "Polski" }, - { code: "HU", name: "Hungarian" }, - { code: "FA", name: "Persian" }, - { code: "RO", name: "Romanian" }, - { code: "GR", name: "Greek" }, - { code: "NL", name: "Dutch" }, - ] - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - ref: main - token: ${{ secrets.GH_TOKEN }} + translation: + runs-on: ubuntu-latest + strategy: + matrix: + language: + [ + { code: "CN", name: "Chinese" }, + { code: "DE", name: "German" }, + { code: "ES", name: "Spanish" }, + { code: "FR", name: "French" }, + { code: "HE", name: "Hebrew" }, + { code: "IT", name: "Italian" }, + { code: "JA", name: "Japanese" }, + { code: "KOR", name: "Korean" }, + { code: "PTBR", name: "Portuguese (Brazil)" }, + { code: "RU", name: "Russian" }, + { code: "TH", name: "Thai" }, + { code: "TR", name: "Turkish" }, + { code: "VI", name: "Vietnamese" }, + { code: "AR", name: "Arabic" }, + { code: "RS", name: "Srpski" }, + { code: "TG", name: "Tagalog" }, + { code: "PL", name: "Polski" }, + { code: "HU", name: "Hungarian" }, + { code: "FA", name: "Persian" }, + { code: "RO", name: "Romanian" }, + { code: "GR", name: "Greek" }, + { code: "NL", name: "Dutch" }, + ] + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.GH_TOKEN }} - - name: Translate to ${{ matrix.language.name }} - uses: 0xjord4n/aixion@v1.2.1 - id: aixion - with: - config: > - { - "provider": "openai", - "provider_options": { - "api_key": "${{ secrets.OPENAI_API_KEY }}" - }, - "messages": [ - { - "role": "system", - "content": "You will be provided with a markdown file in English, and your task is to translate it into ${{ matrix.language.name }}." - }, - { - "role": "user", - "content_path": "README.md" - } - ], - save_path: "packages/docs/packages/docs/i18n/readme/README_${{ matrix.language.code }}.md", - "model": "gpt-4o" - } + - name: Translate to ${{ matrix.language.name }} + uses: 0xjord4n/aixion@v1.2.1 + id: aixion + with: + config: > + { + "provider": "openai", + "provider_options": { + "api_key": "${{ secrets.OPENAI_API_KEY }}" + }, + "messages": [ + { + "role": "system", + "content": "You will be provided with a markdown file in English, and your task is to translate it into ${{ matrix.language.name }}." + }, + { + "role": "user", + "content_path": "README.md" + } + ], + save_path: "packages/docs/packages/docs/i18n/readme/README_${{ matrix.language.code }}.md", + "model": "gpt-4o" + } - # Upload each translated file as an artifact - - name: Upload translation - uses: actions/upload-artifact@v4 - with: - name: readme-${{ matrix.language.code }} - path: README_${{ matrix.language.code }}.md + # Upload each translated file as an artifact + - name: Upload translation + uses: actions/upload-artifact@v4 + with: + name: readme-${{ matrix.language.code }} + path: README_${{ matrix.language.code }}.md - commit: - needs: translation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: main - token: ${{ secrets.GH_TOKEN }} + commit: + needs: translation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.GH_TOKEN }} - # Download all translation artifacts - - name: Download all translations - uses: actions/download-artifact@v4 - with: - pattern: readme-* - merge-multiple: true + # Download all translation artifacts + - name: Download all translations + uses: actions/download-artifact@v4 + with: + pattern: readme-* + merge-multiple: true - - name: Commit all translations - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: update all README translations" - branch: main - file_pattern: "README_*.md" - commit_author: "GitHub Action " + - name: Commit all translations + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update all README translations" + branch: main + file_pattern: "README_*.md" + commit_author: "GitHub Action " diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index 2135ed59c83..c9a8cc518f2 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -3,68 +3,68 @@ name: Create and publish a Docker image # Configures this workflow to run every time a change is pushed to the branch called `release`. on: - release: - types: [created] - workflow_dispatch: + release: + types: [created] + workflow_dispatch: # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. jobs: - build-and-push-image: - runs-on: ubuntu-latest - # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. - permissions: - contents: read - packages: write - attestations: write - id-token: write - # - steps: - - name: Checkout repository - uses: actions/checkout@v4 - # Uses the `docker/login-action` action to log in to the Container registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. - # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. - # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. - - name: Build and push Docker image - id: push - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} - # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true - # This step makes the Docker image public, so users can pull it without authentication. - - name: Make Docker image public - run: | - curl \ - -X PATCH \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/user/packages/container/${{ env.IMAGE_NAME }}/visibility \ - -d '{"visibility":"public"}' + # This step makes the Docker image public, so users can pull it without authentication. + - name: Make Docker image public + run: | + curl \ + -X PATCH \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/user/packages/container/${{ env.IMAGE_NAME }}/visibility \ + -d '{"visibility":"public"}' diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index fb67a97dbce..ff90535fcc9 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -1,30 +1,30 @@ name: PR Title Check on: - pull_request: - types: [opened, edited, synchronize] + pull_request: + types: [opened, edited, synchronize] jobs: - check-pr-title: - runs-on: ubuntu-latest + check-pr-title: + runs-on: ubuntu-latest - steps: - - name: Check out the repository - uses: actions/checkout@v4 + steps: + - name: Check out the repository + uses: actions/checkout@v4 - - name: Validate PR title - id: validate - run: | - PR_TITLE=$(jq -r .pull_request.title "$GITHUB_EVENT_PATH") - echo "PR Title: $PR_TITLE" - if [[ ! "$PR_TITLE" =~ ^(feat|fix|docs|style|refactor|test|chore)(\([a-zA-Z0-9-]+\))?:\ .+ ]]; then - echo "PR title does not match the required pattern." - exit 1 - fi + - name: Validate PR title + id: validate + run: | + PR_TITLE=$(jq -r .pull_request.title "$GITHUB_EVENT_PATH") + echo "PR Title: $PR_TITLE" + if [[ ! "$PR_TITLE" =~ ^(feat|fix|docs|style|refactor|test|chore)(\([a-zA-Z0-9-]+\))?:\ .+ ]]; then + echo "PR title does not match the required pattern." + exit 1 + fi - - name: Set status - if: failure() - run: | - gh pr comment ${{ github.event.pull_request.number }} --body "❌ PR title does not match the required pattern. Please use one of these formats: - - 'type: description' (e.g., 'feat: add new feature') - - 'type(scope): description' (e.g., 'chore(core): update dependencies')" + - name: Set status + if: failure() + run: | + gh pr comment ${{ github.event.pull_request.number }} --body "❌ PR title does not match the required pattern. Please use one of these formats: + - 'type: description' (e.g., 'feat: add new feature') + - 'type(scope): description' (e.g., 'chore(core): update dependencies')" diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 71c6b53dc7a..dcff059b229 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -1,73 +1,73 @@ name: Pre-Release on: - workflow_dispatch: - inputs: - release_type: - description: "Type of release (prerelease, prepatch, patch, minor, preminor, major)" - required: true - default: "prerelease" + workflow_dispatch: + inputs: + release_type: + description: "Type of release (prerelease, prepatch, patch, minor, preminor, major)" + required: true + default: "prerelease" jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: "23" + - uses: actions/setup-node@v4 + with: + node-version: "23" - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@v2 - - name: Configure Git - run: | - git config user.name "${{ github.actor }}" - git config user.email "${{ github.actor }}@users.noreply.github.com" + - name: Configure Git + run: | + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" - - name: "Setup npm for npmjs" - run: | - npm config set registry https://registry.npmjs.org/ - echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + - name: "Setup npm for npmjs" + run: | + npm config set registry https://registry.npmjs.org/ + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - - name: Install Protobuf Compiler - run: sudo apt-get install -y protobuf-compiler + - name: Install Protobuf Compiler + run: sudo apt-get install -y protobuf-compiler - - name: Install dependencies - run: bun install + - name: Install dependencies + run: bun install - - name: Build packages - run: bun run build + - name: Build packages + run: bun run build - - name: Tag and Publish Packages - id: tag_publish - run: | - RELEASE_TYPE=${{ github.event_name == 'push' && 'prerelease' || github.event.inputs.release_type }} - npx lerna version $RELEASE_TYPE --conventional-commits --yes --no-private --force-publish - npx lerna publish from-git --yes --dist-tag next + - name: Tag and Publish Packages + id: tag_publish + run: | + RELEASE_TYPE=${{ github.event_name == 'push' && 'prerelease' || github.event.inputs.release_type }} + npx lerna version $RELEASE_TYPE --conventional-commits --yes --no-private --force-publish + npx lerna publish from-git --yes --dist-tag next - - name: Get Version Tag - id: get_tag - run: echo "TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT + - name: Get Version Tag + id: get_tag + run: echo "TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT - - name: Generate Release Body - id: release_body - run: | - if [ -f CHANGELOG.md ]; then - echo "body=$(cat CHANGELOG.md)" >> $GITHUB_OUTPUT - else - echo "body=No changelog provided for this release." >> $GITHUB_OUTPUT - fi + - name: Generate Release Body + id: release_body + run: | + if [ -f CHANGELOG.md ]; then + echo "body=$(cat CHANGELOG.md)" >> $GITHUB_OUTPUT + else + echo "body=No changelog provided for this release." >> $GITHUB_OUTPUT + fi - - name: Create GitHub Release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - with: - tag_name: ${{ steps.get_tag.outputs.TAG }} - release_name: Release - body_path: CHANGELOG.md - draft: false - prerelease: ${{ github.event_name == 'push' }} + - name: Create GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + with: + tag_name: ${{ steps.get_tag.outputs.TAG }} + release_name: Release + body_path: CHANGELOG.md + draft: false + prerelease: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6e7984e0a20..2d1a38af33d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,59 +1,59 @@ name: Release on: - release: - types: [created] - workflow_dispatch: + release: + types: [created] + workflow_dispatch: jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: "23" - - - uses: oven-sh/setup-bun@v2 - - - name: Configure Git - run: | - git config user.name "${{ github.actor }}" - git config user.email "${{ github.actor }}@users.noreply.github.com" - - - name: "Setup npm for npmjs" - run: | - npm config set registry https://registry.npmjs.org/ - echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - - - name: Install Protobuf Compiler - run: sudo apt-get install -y protobuf-compiler - - - name: Install dependencies - run: bun install - - - name: Build packages - run: bun run build - - - name: Publish Packages - id: publish - run: | - # Get the latest release tag - LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) - - # Force clean the working directory and reset any changes - echo "Cleaning working directory and resetting any changes" - git clean -fd - git reset --hard HEAD - - # Force checkout the latest tag - echo "Checking out latest tag: $LATEST_TAG" - git checkout -b temp-publish-branch $LATEST_TAG - - echo "Publishing version: $LATEST_TAG" - npx lerna publish from-package --yes --dist-tag latest - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: "23" + + - uses: oven-sh/setup-bun@v2 + + - name: Configure Git + run: | + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" + + - name: "Setup npm for npmjs" + run: | + npm config set registry https://registry.npmjs.org/ + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + + - name: Install Protobuf Compiler + run: sudo apt-get install -y protobuf-compiler + + - name: Install dependencies + run: bun install + + - name: Build packages + run: bun run build + + - name: Publish Packages + id: publish + run: | + # Get the latest release tag + LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) + + # Force clean the working directory and reset any changes + echo "Cleaning working directory and resetting any changes" + git clean -fd + git reset --hard HEAD + + # Force checkout the latest tag + echo "Checking out latest tag: $LATEST_TAG" + git checkout -b temp-publish-branch $LATEST_TAG + + echo "Publishing version: $LATEST_TAG" + npx lerna publish from-package --yes --dist-tag latest + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/biome.json b/biome.json index 125fb2ecf25..0f3a2a354ae 100644 --- a/biome.json +++ b/biome.json @@ -47,7 +47,15 @@ "**/dist/**", "**/node_modules/**", "**/coverage/**", - "**/*.json" + "**/*.md", + "**/.docusaurus/**", + "**/build/**", + "**/*.json", + "**/.turbo/**", + "**/.git/**", + "**/local_cache/**", + "**/cache/**", + "**/tmp/**" ] }, "formatter": { diff --git a/package.json b/package.json index 3ed3971afec..bc504c4c3b9 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "eliza", "scripts": { - "format": "biome format --write .", + "format": "biome format .", "cli": "bun --filter=@elizaos/cli cli", "lint": "turbo run lint:fix --filter=./packages/*", - "check": "biome check --write .", + "check": "biome check . --fix --unsafe --verbose", "preinstall": "npx only-allow bun", "swarm": "turbo run build --filter=./packages/core && concurrently \"turbo run start --filter=@elizaos/agent -- --swarm \" \"turbo run start --filter=!@elizaos/agent --filter=!@elizaos/docs --filter=!@elizaos/core\"", "swarm:scenario": "turbo run build --filter=./packages/core && concurrently \"turbo run start --filter=@elizaos/agent -- --swarm --scenario \" \"turbo run start --filter=!@elizaos/agent --filter=!@elizaos/docs --filter=!@elizaos/core\"", diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index c1ad070ccd7..10598dbea3d 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -152,7 +152,6 @@ async function startAgent( let db: IDatabaseAdapter & IDatabaseCacheAdapter; try { character.id ??= stringToUuid(character.name); - character.username ??= character.name; const runtime: IAgentRuntime = await createAgent(character); diff --git a/packages/agent/src/plugins.test.ts b/packages/agent/src/plugins.test.ts index 6d70726e06e..a713b97f122 100644 --- a/packages/agent/src/plugins.test.ts +++ b/packages/agent/src/plugins.test.ts @@ -192,7 +192,7 @@ class TestRunner { if (!this.testResults.has(file)) { this.testResults.set(file, []); } - this.testResults.get(file)!.push({ file, suite, name, status, error }); + this.testResults.get(file)?.push({ file, suite, name, status, error }); } private async runTestSuite(suite: TestSuite, file: string): Promise { @@ -275,7 +275,7 @@ class TestRunner { if (!groupedBySuite.has(t.suite)) { groupedBySuite.set(t.suite, []); } - groupedBySuite.get(t.suite)!.push(t); + groupedBySuite.get(t.suite)?.push(t); }); groupedBySuite.forEach((suiteTests, suite) => { @@ -302,8 +302,8 @@ class TestRunner { tests.forEach(test => { if (test.status === "failed") { console.log(` ${colorize("FAIL", "red")} ${test.file} > ${test.suite} > ${test.name}`); - console.log(` ${colorize("AssertionError: " + test.error!.message, "red")}`); - console.log("\n" + colorize("⎯".repeat(66), "red") + "\n"); + console.log(` ${colorize(`AssertionError: ${test.error?.message}`, "red")}`); + console.log(`\n${colorize("⎯".repeat(66), "red")}\n`); } }); }); @@ -312,8 +312,8 @@ class TestRunner { const printTestSummary = (failedTestSuites: number) => { printSectionHeader("Test Summary", "cyan"); - console.log(` ${colorize("Test Suites:", "gray")} ${failedTestSuites > 0 ? colorize(failedTestSuites + " failed | ", "red") : ""}${colorize((this.testResults.size - failedTestSuites) + " passed", "green")} (${this.testResults.size})`); - console.log(` ${colorize(" Tests:", "gray")} ${this.stats.failed > 0 ? colorize(this.stats.failed + " failed | ", "red") : ""}${colorize(this.stats.passed + " passed", "green")} (${this.stats.total})`); + console.log(` ${colorize("Test Suites:", "gray")} ${failedTestSuites > 0 ? colorize(`${failedTestSuites} failed | `, "red") : ""}${colorize(`${this.testResults.size - failedTestSuites} passed`, "green")} (${this.testResults.size})`); + console.log(` ${colorize(" Tests:", "gray")} ${this.stats.failed > 0 ? colorize(`${this.stats.failed} failed | `, "red") : ""}${colorize(`${this.stats.passed} passed`, "green")} (${this.stats.total})`); }; const failedTestSuites = printTestSuiteSummary(); diff --git a/packages/agent/src/server/api/agent.ts b/packages/agent/src/server/api/agent.ts index 8edc98ef2c3..0a52f0cd333 100644 --- a/packages/agent/src/server/api/agent.ts +++ b/packages/agent/src/server/api/agent.ts @@ -1,14 +1,11 @@ +import type { Character, Content, IAgentRuntime, Media, Memory } from '@elizaos/core'; +import { ChannelType, composeContext, createUniqueUuid, generateMessageResponse, logger, messageHandlerTemplate, ModelClass, validateCharacterConfig } from '@elizaos/core'; import express from 'express'; -import type { Character, IAgentRuntime, Media } from '@elizaos/core'; -import { ChannelType, composeContext, generateMessageResponse, logger, ModelClass, stringToUuid, validateCharacterConfig } from '@elizaos/core'; import fs from 'node:fs'; -import type { AgentServer } from '..'; -import { validateUUIDParams } from './api-utils'; - -import type { Content, Memory } from '@elizaos/core'; import path from 'node:path'; -import { messageHandlerTemplate } from '../helper'; +import type { AgentServer } from '..'; import { upload } from '../loader'; +import { validateUUIDParams } from './api-utils'; interface CustomRequest extends express.Request { file?: Express.Multer.File; @@ -96,8 +93,8 @@ export function agentRouter( return; } - const roomId = stringToUuid(req.body.roomId ?? `default-room-${agentId}`); - const userId = stringToUuid(req.body.userId ?? "user"); + const roomId = createUniqueUuid(this.runtime, req.body.roomId ?? `default-room-${agentId}`); + const userId = createUniqueUuid(this.runtime, req.body.userId ?? "user"); let runtime = agents.get(agentId); @@ -129,7 +126,7 @@ export function agentRouter( logger.info(`[MESSAGE ENDPOINT] req.body: ${JSON.stringify(req.body)}`); - const messageId = stringToUuid(Date.now().toString()); + const messageId = createUniqueUuid(this.runtime, Date.now().toString()); const attachments: Media[] = []; if (req.file) { @@ -165,7 +162,7 @@ export function agentRouter( }; const memory: Memory = { - id: stringToUuid(`${messageId}-${userId}`), + id: createUniqueUuid(this.runtime, messageId), ...userMessage, agentId: runtime.agentId, userId, @@ -205,7 +202,7 @@ export function agentRouter( // save response to memory const responseMessage: Memory = { - id: stringToUuid(`${messageId}-${runtime.agentId}`), + id: createUniqueUuid(runtime, messageId), ...userMessage, userId: runtime.agentId, content: response, @@ -386,7 +383,7 @@ export function agentRouter( if (!character) { try { character = await directClient.loadCharacterTryPath(characterName); - } catch (e) { + } catch (_e) { const existingAgent = Array.from(agents.values()).find( (a) => a.character.name.toLowerCase() === characterName.toLowerCase() ); @@ -464,8 +461,8 @@ export function agentRouter( if (!agentId) return; const { text, roomId: rawRoomId, userId: rawUserId } = req.body; - const roomId = stringToUuid(rawRoomId ?? `default-room-${agentId}`); - const userId = stringToUuid(rawUserId ?? "user"); + const roomId = createUniqueUuid(this.runtime, rawRoomId ?? `default-room-${agentId}`); + const userId = createUniqueUuid(this.runtime, rawUserId ?? "user"); if (!text) { res.status(400).send("No text provided"); @@ -495,7 +492,7 @@ export function agentRouter( type: ChannelType.API, }); - const messageId = stringToUuid(Date.now().toString()); + const messageId = createUniqueUuid(this.runtime, Date.now().toString()); const content: Content = { text, diff --git a/packages/agent/src/server/helper.ts b/packages/agent/src/server/helper.ts index 00878e5f03e..d1d8682b777 100644 --- a/packages/agent/src/server/helper.ts +++ b/packages/agent/src/server/helper.ts @@ -1,42 +1,12 @@ -import { messageCompletionFooter } from "@elizaos/core"; - -export const messageHandlerTemplate = - `{{actionExamples}} -(Action examples are for reference only. Do not use the information from them in your response.) - -# Knowledge -{{knowledge}} - -# Task: Generate dialog and actions for the character {{agentName}}. -About {{agentName}}: -{{bio}} - -{{system}} - -{{providers}} - -{{attachments}} - -# Capabilities -Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. - -{{messageDirections}} - -{{recentMessages}} - -{{actions}} - -# Instructions: Write the next message for {{agentName}}. -${messageCompletionFooter}`; - export const hyperfiHandlerTemplate = `Task: Generate dialog and actions for the character {{agentName}}. {{actionExamples}} (Action examples are for reference only. Do not use the information from them in your response.) -# Knowledge {{knowledge}} +{{actors}} + About {{agentName}}: {{bio}} diff --git a/packages/agent/src/single-agent/character.ts b/packages/agent/src/single-agent/character.ts index 8c768e1c367..3df3089b5a0 100644 --- a/packages/agent/src/single-agent/character.ts +++ b/packages/agent/src/single-agent/character.ts @@ -6,7 +6,6 @@ dotenv.config({ export const defaultCharacter: Character = { name: "Eliza", - username: "eliza", plugins: [ "@elizaos/plugin-anthropic", "@elizaos/plugin-openai", diff --git a/packages/agent/src/swarm/scenario.ts b/packages/agent/src/swarm/scenario.ts index 740f0e9502d..eaf2bd009b9 100644 --- a/packages/agent/src/swarm/scenario.ts +++ b/packages/agent/src/swarm/scenario.ts @@ -1,11 +1,11 @@ import { ChannelType, - Client, - HandlerCallback, - IAgentRuntime, - Memory, - UUID, - stringToUuid, + type Client, + type HandlerCallback, + type IAgentRuntime, + type Memory, + type UUID, + createUniqueUuid } from "@elizaos/core"; import { v4 as uuidv4 } from "uuid"; @@ -49,13 +49,13 @@ export class ScenarioClient implements Client { ) { for (const receiver of receivers) { - const participantId = stringToUuid(sender.agentId + "-" + receiver.agentId); const roomData = this.rooms.get(receiver.agentId); if (!roomData) continue; - + const userId = createUniqueUuid(receiver, sender.agentId) + // Ensure connection exists await receiver.ensureConnection({ - userId: participantId, + userId, roomId: roomData.roomId, userName: sender.character.name, userScreenName: sender.character.name, @@ -64,7 +64,7 @@ export class ScenarioClient implements Client { }); const memory: Memory = { - userId: participantId, + userId, agentId: receiver.agentId, roomId: roomData.roomId, content: { @@ -87,14 +87,15 @@ export class ScenarioClient implements Client { ) { for (const receiver of receivers) { - const participantId = stringToUuid(sender.agentId + "-" + receiver.agentId); const roomData = this.rooms.get(receiver.agentId); if (!roomData) continue; + + const userId = createUniqueUuid(receiver, sender.agentId); if (receiver.agentId !== sender.agentId) { // Ensure connection exists await receiver.ensureConnection({ - userId: participantId, + userId, roomId: roomData.roomId, userName: sender.character.name, userScreenName: sender.character.name, @@ -113,7 +114,7 @@ export class ScenarioClient implements Client { } const memory: Memory = { - userId: receiver.agentId !== sender.agentId ? participantId : sender.agentId, + userId: receiver.agentId !== sender.agentId ? userId : sender.agentId, agentId: receiver.agentId, roomId: roomData.roomId, content: { @@ -128,7 +129,7 @@ export class ScenarioClient implements Client { runtime: receiver, message: memory, roomId: roomData.roomId, - userId: receiver.agentId !== sender.agentId ? participantId : sender.agentId, + userId: receiver.agentId !== sender.agentId ? userId : sender.agentId, source: "scenario", type: ChannelType.GROUP, }); diff --git a/packages/agent/src/swarm/settings.ts b/packages/agent/src/swarm/settings.ts index a8744754333..670f0694ac8 100644 --- a/packages/agent/src/swarm/settings.ts +++ b/packages/agent/src/swarm/settings.ts @@ -1,14 +1,14 @@ import { - Action, + type Action, ChannelType, - Evaluator, + createUniqueUuid, + type Evaluator, type IAgentRuntime, initializeOnboardingConfig, logger, type OnboardingConfig, - Provider, + type Provider, RoleName, - stringToUuid, type UUID } from "@elizaos/core"; import type { Guild } from "discord.js"; @@ -77,13 +77,8 @@ export async function initializeAllSystems( try { for (const server of servers) { - const worldId = stringToUuid(`${server.id}-${runtime.agentId}`); - - const ownerId = stringToUuid( - `${server.ownerId}-${runtime.agentId}` - ); - - const tenantSpecificOwnerId = runtime.generateTenantUserId(ownerId); + const worldId = createUniqueUuid(runtime, server.id); + const ownerId = createUniqueUuid(runtime, server.ownerId); await runtime.ensureWorldExists({ id: worldId, @@ -93,7 +88,7 @@ export async function initializeAllSystems( metadata: { ownership: server.ownerId ? { ownerId } : undefined, roles: { - [tenantSpecificOwnerId]: RoleName.OWNER, + [ownerId]: RoleName.OWNER, }, } }); @@ -151,7 +146,7 @@ export async function startOnboardingDM( const randomMessage = onboardingMessages[Math.floor(Math.random() * onboardingMessages.length)]; const msg = await owner.send(randomMessage); - const roomId = stringToUuid(`${msg.channel.id}-${runtime.agentId}`); + const roomId = createUniqueUuid(runtime, msg.channel.id); await runtime.ensureRoomExists({ id: roomId, @@ -165,9 +160,13 @@ export async function startOnboardingDM( await runtime.getOrCreateUser( runtime.agentId, - runtime.character.name, - runtime.character.name, - "discord" + [runtime.character.name], + { + default: { + name: runtime.character.name, + userName: runtime.character.name, + }, + }, ); // Create memory of the initial message diff --git a/packages/agent/src/swarm/socialMediaManager/actions/post.ts b/packages/agent/src/swarm/socialMediaManager/actions/post.ts index 763e3b8fe37..07752bd5339 100644 --- a/packages/agent/src/swarm/socialMediaManager/actions/post.ts +++ b/packages/agent/src/swarm/socialMediaManager/actions/post.ts @@ -9,11 +9,10 @@ import { RoleName, type State, composeContext, + createUniqueUuid, generateText, getWorldSettings, - logger, - normalizeUserId, - stringToUuid + logger } from "@elizaos/core"; /** @@ -25,18 +24,15 @@ export async function getUserServerRole( serverId: string ): Promise { try { - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(this.runtime, serverId); const world = await runtime.getWorld(worldId); if (!world || !world.metadata?.roles) { return RoleName.NONE; } - // Check both formats (UUID and original ID) - const normalizedUserId = normalizeUserId(userId); - - if (world.metadata.roles[normalizedUserId]?.role) { - return world.metadata.roles[normalizedUserId].role as RoleName; + if (world.metadata.roles[userId]?.role) { + return world.metadata.roles[userId].role as RoleName; } // Also check original ID format @@ -245,7 +241,7 @@ const twitterPostAction: Action = { // Check if there are any pending Twitter posts awaiting confirmation const pendingTasks = runtime.getTasks({ roomId: message.roomId, - tags: ["AWAITING_CONFIRMATION", "TWITTER_POST"], + tags: ["TWITTER_POST"], }); if (pendingTasks && pendingTasks.length > 0) { @@ -266,8 +262,38 @@ const twitterPostAction: Action = { roomId: message.roomId, name: "Confirm Twitter Post", description: "Confirm the tweet to be posted.", - tags: ["TWITTER_POST", "AWAITING_CONFIRMATION"], - handler: async (runtime: IAgentRuntime) => { + tags: ["TWITTER_POST", "AWAITING_CHOICE"], + metadata: { + options: [ + { + name: "post", + description: "Post the tweet to Twitter", + }, + { + name: "cancel", + description: "Cancel the tweet and don't post it", + }, + ], + }, + handler: async (runtime: IAgentRuntime, options: { option: string }) => { + if (options.option === "cancel") { + await callback({ + ...responseContent, + text: "Tweet cancelled. I won't post it.", + action: "TWITTER_POST_CANCELLED" + }); + return; + } + + if(options.option !== "post") { + await callback({ + ...responseContent, + text: "Invalid option. Should be 'post' or 'cancel'.", + action: "TWITTER_POST_INVALID_OPTION" + }); + return; + } + const vals = { TWITTER_USERNAME: worldSettings.TWITTER_USERNAME.value, TWITTER_EMAIL: worldSettings.TWITTER_EMAIL.value, diff --git a/packages/cli/src/commands/character.ts b/packages/cli/src/commands/character.ts index c6d37e110ac..aa9bf942aef 100644 --- a/packages/cli/src/commands/character.ts +++ b/packages/cli/src/commands/character.ts @@ -24,7 +24,6 @@ import { withConnection } from "../utils/with-connection"; const characterSchema = z.object({ id: z.string().uuid().optional(), name: z.string(), - username: z.string(), plugins: z.array(z.string()).optional(), secrets: z.record(z.string(), z.string()).optional(), bio: z.array(z.string()).optional(), @@ -376,7 +375,6 @@ character.command("create") const charData = { ...getDefaultCharacterFields(), name: formData.name, - username: formData.username, bio: formData.bio, adjectives: formData.adjectives, postExamples: formData.postExamples, @@ -428,7 +426,6 @@ character.command("edit") const formData = await collectCharacterData({ name: existing.name, - username: existing.username, bio: Array.isArray(existing.bio) ? existing.bio : [existing.bio], adjectives: existing.adjectives || [], postExamples: existing.postExamples || [], diff --git a/packages/cli/src/utils/install-plugin.ts b/packages/cli/src/utils/install-plugin.ts index 6dfdbbe6a2a..0241d5f6501 100644 --- a/packages/cli/src/utils/install-plugin.ts +++ b/packages/cli/src/utils/install-plugin.ts @@ -2,7 +2,7 @@ import { execa } from "execa"; import path from "node:path"; import { logger } from "@/src/utils/logger"; import { runBunCommand } from "@/src/utils/run-bun"; -import { promises as fs } from "fs"; +import { promises as fs } from "node:fs"; export async function installPlugin( pluginName: string, diff --git a/packages/client/src/components/overview.tsx b/packages/client/src/components/overview.tsx index 473fe6717b3..70d148e7bf9 100644 --- a/packages/client/src/components/overview.tsx +++ b/packages/client/src/components/overview.tsx @@ -251,7 +251,7 @@ export default function Overview({ character }: { character: Character }) { title: "Success", description: "Character updated successfully", }); - } catch (error) { + } catch (_error) { toast({ title: "Error", description: "Failed to update character", diff --git a/packages/client/tsconfig.app.tsbuildinfo b/packages/client/tsconfig.app.tsbuildinfo index f4f70251c18..0cb74826b7e 100644 --- a/packages/client/tsconfig.app.tsbuildinfo +++ b/packages/client/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/app-sidebar.tsx","./src/components/array-input.tsx","./src/components/audio-recorder.tsx","./src/components/chat.tsx","./src/components/connection-status.tsx","./src/components/copy-button.tsx","./src/components/input-copy.tsx","./src/components/overview.tsx","./src/components/page-title.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/chat/chat-bubble.tsx","./src/components/ui/chat/chat-input.tsx","./src/components/ui/chat/chat-message-list.tsx","./src/components/ui/chat/chat-tts-button.tsx","./src/components/ui/chat/expandable-chat.tsx","./src/components/ui/chat/message-loading.tsx","./src/components/ui/chat/hooks/useautoscroll.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/use-version.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/routes/chat.tsx","./src/routes/home.tsx","./src/routes/overview.tsx","./src/types/index.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/app-sidebar.tsx","./src/components/array-input.tsx","./src/components/audio-recorder.tsx","./src/components/chat.tsx","./src/components/connection-status.tsx","./src/components/copy-button.tsx","./src/components/input-copy.tsx","./src/components/overview.tsx","./src/components/page-title.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/chat/chat-bubble.tsx","./src/components/ui/chat/chat-input.tsx","./src/components/ui/chat/chat-message-list.tsx","./src/components/ui/chat/chat-tts-button.tsx","./src/components/ui/chat/expandable-chat.tsx","./src/components/ui/chat/message-loading.tsx","./src/components/ui/chat/hooks/useautoscroll.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-plugins.ts","./src/hooks/use-toast.ts","./src/hooks/use-version.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/routes/chat.tsx","./src/routes/home.tsx","./src/routes/overview.tsx","./src/types/index.ts"],"version":"5.6.3"} \ No newline at end of file diff --git a/packages/core/__tests__/goals.test.ts b/packages/core/__tests__/goals.test.ts index ea698f7e705..e4f12bfe4c5 100644 --- a/packages/core/__tests__/goals.test.ts +++ b/packages/core/__tests__/goals.test.ts @@ -7,25 +7,25 @@ import { updateGoal, } from "../src/goals.ts"; import { - Action, - ChannelType, + type Action, + type ChannelType, type Character, - Client, + type Client, type Goal, GoalStatus, - HandlerCallback, + type HandlerCallback, type IAgentRuntime, type IMemoryManager, type Memory, - ModelClass, - Provider, - RoomData, + type ModelClass, + type Provider, + type RoomData, type Service, type ServiceType, type State, - Task, + type Task, type UUID, - WorldData + type WorldData } from "../src/types.ts"; // Mock the database adapter @@ -35,7 +35,7 @@ export const mockDatabaseAdapter = { createGoal: mock(), }; -const services = new Map(); +const _services = new Map(); // Create memory managers first const messageManager: IMemoryManager = { @@ -102,91 +102,91 @@ export const mockRuntime: IAgentRuntime = { getMemoryManager: () => null, getModel: () => undefined, events: new Map(), - registerClientInterface: function (name: string, client: Client): void { + registerClientInterface: (_name: string, _client: Client): void => { throw new Error("Function not implemented."); }, - transformUserId: function (userId: UUID): UUID { + transformUserId: (_userId: UUID): UUID => { throw new Error("Function not implemented."); }, - registerService: function (service: Service): void { + registerService: (_service: Service): void => { throw new Error("Function not implemented."); }, - setSetting: function (key: string, value: string | boolean | null | any, secret: boolean): void { + setSetting: (_key: string, _value: string | boolean | null | any, _secret: boolean): void => { throw new Error("Function not implemented."); }, - getSetting: function (key: string) { + getSetting: (_key: string) => { throw new Error("Function not implemented."); }, - getConversationLength: function (): number { + getConversationLength: (): number => { throw new Error("Function not implemented."); }, - processActions: function (message: Memory, responses: Memory[], state?: State, callback?: HandlerCallback): Promise { + processActions: (_message: Memory, _responses: Memory[], _state?: State, _callback?: HandlerCallback): Promise => { throw new Error("Function not implemented."); }, - evaluate: function (message: Memory, state?: State, didRespond?: boolean, callback?: HandlerCallback): Promise { + evaluate: (_message: Memory, _state?: State, _didRespond?: boolean, _callback?: HandlerCallback): Promise => { throw new Error("Function not implemented."); }, - getOrCreateUser: function (userId: UUID, userName: string | null, name: string | null, source: string | null): Promise { + getOrCreateUser: (_userId: UUID, _userName: string | null, _name: string | null, _source: string | null): Promise => { throw new Error("Function not implemented."); }, - registerProvider: function (provider: Provider): void { + registerProvider: (_provider: Provider): void => { throw new Error("Function not implemented."); }, - registerAction: function (action: Action): void { + registerAction: (_action: Action): void => { throw new Error("Function not implemented."); }, - ensureConnection: function ({ userId, roomId, userName, userScreenName, source, channelId, serverId, type, }: { userId: UUID; roomId: UUID; userName?: string; userScreenName?: string; source?: string; channelId?: string; serverId?: string; type: ChannelType; }): Promise { + ensureConnection: ({ userId, roomId, userName, userScreenName, source, channelId, serverId, type, }: { userId: UUID; roomId: UUID; userName?: string; userScreenName?: string; source?: string; channelId?: string; serverId?: string; type: ChannelType; }): Promise => { throw new Error("Function not implemented."); }, - ensureParticipantInRoom: function (userId: UUID, roomId: UUID): Promise { + ensureParticipantInRoom: (_userId: UUID, _roomId: UUID): Promise => { throw new Error("Function not implemented."); }, - getWorld: function (worldId: UUID): Promise { + getWorld: (_worldId: UUID): Promise => { throw new Error("Function not implemented."); }, - ensureWorldExists: function ({ id, name, serverId, }: WorldData): Promise { + ensureWorldExists: ({ id, name, serverId, }: WorldData): Promise => { throw new Error("Function not implemented."); }, - getRoom: function (roomId: UUID): Promise { + getRoom: (_roomId: UUID): Promise => { throw new Error("Function not implemented."); }, - registerModel: function (modelClass: ModelClass, handler: (params: any) => Promise): void { + registerModel: (_modelClass: ModelClass, _handler: (params: any) => Promise): void => { throw new Error("Function not implemented."); }, - registerEvent: function (event: string, handler: (params: any) => void): void { + registerEvent: (_event: string, _handler: (params: any) => void): void => { throw new Error("Function not implemented."); }, - getEvent: function (event: string): ((params: any) => void)[] | undefined { + getEvent: (_event: string): ((params: any) => void)[] | undefined => { throw new Error("Function not implemented."); }, - emitEvent: function (event: string | string[], params: any): void { + emitEvent: (_event: string | string[], _params: any): void => { throw new Error("Function not implemented."); }, - registerTask: function (task: Task): UUID { + registerTask: (_task: Task): UUID => { throw new Error("Function not implemented."); }, - getTasks: function ({ roomId, tags, }: { roomId?: UUID; tags?: string[]; }): Task[] | undefined { + getTasks: ({ roomId, tags, }: { roomId?: UUID; tags?: string[]; }): Task[] | undefined => { throw new Error("Function not implemented."); }, - getTask: function (id: UUID): Task | undefined { + getTask: (_id: UUID): Task | undefined => { throw new Error("Function not implemented."); }, - updateTask: function (id: UUID, task: Task): void { + updateTask: (_id: UUID, _task: Task): void => { throw new Error("Function not implemented."); }, - deleteTask: function (id: UUID): void { + deleteTask: (_id: UUID): void => { throw new Error("Function not implemented."); }, - stop: function (): Promise { + stop: (): Promise => { throw new Error("Function not implemented."); }, - ensureAgentExists: function (): Promise { + ensureAgentExists: (): Promise => { throw new Error("Function not implemented."); }, - ensureEmbeddingDimension: function (): Promise { + ensureEmbeddingDimension: (): Promise => { throw new Error("Function not implemented."); }, - ensureCharacterExists: function (character: Character): Promise { + ensureCharacterExists: (_character: Character): Promise => { throw new Error("Function not implemented."); } }; diff --git a/packages/core/__tests__/knowledge.test.ts b/packages/core/__tests__/knowledge.test.ts index 5ba21456a05..1d181a2e5bb 100644 --- a/packages/core/__tests__/knowledge.test.ts +++ b/packages/core/__tests__/knowledge.test.ts @@ -228,7 +228,7 @@ describe("Knowledge Module", () => { const largeText = "test ".repeat(1000); // ~5000 chars // Mock splitChunks to return more chunks for large text - mockSplitChunks.mockImplementation(async (text: string) => { + mockSplitChunks.mockImplementation(async (_text: string) => { // Create ~7 chunks for the test return Array.from({ length: 7 }, (_, i) => `chunk${i + 1} of large text` @@ -279,7 +279,7 @@ describe("Knowledge Module", () => { ]; for (const config of configs) { - const result = await knowledge.set(mockRuntime, { + const _result = await knowledge.set(mockRuntime, { id: TEST_UUID_1, content: { text } }, config); diff --git a/packages/core/__tests__/memory.test.ts b/packages/core/__tests__/memory.test.ts index ea60c431f56..c8721da6523 100644 --- a/packages/core/__tests__/memory.test.ts +++ b/packages/core/__tests__/memory.test.ts @@ -376,12 +376,12 @@ describe("MemoryManager", () => { }); it("should preserve semantic boundaries when possible", async () => { - const text = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph."; + const _text = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph."; // Test that fragments break at paragraph boundaries }); it("should handle multilingual content properly", async () => { - const multilingualText = "English text. 中文文本. Русский текст."; + const _multilingualText = "English text. 中文文本. Русский текст."; // Test proper handling of different scripts }); }); diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts deleted file mode 100644 index 2001da288ba..00000000000 --- a/packages/core/src/actions/cancel.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { logger } from "../logger"; -import { Action, ActionExample, HandlerCallback, IAgentRuntime, Memory, State } from "../types"; - -export const cancelTaskAction: Action = { - name: "CANCEL_TASK", - similes: ["REJECT_TASK", "STOP_TASK", "NEVERMIND", "CANCEL", "ABORT"], - description: "Cancels a pending task that's awaiting confirmation", - - validate: async ( - runtime: IAgentRuntime, - message: Memory, - _state: State - ): Promise => { - const pendingTasks = runtime.getTasks({ - roomId: message.roomId, - tags: ["AWAITING_CONFIRMATION"], - }); - - // Only validate if there are pending tasks - return pendingTasks && pendingTasks.length > 0; - }, - - handler: async ( - runtime: IAgentRuntime, - message: Memory, - _state: State, - _options: any, - callback: HandlerCallback, - responses: Memory[] - ): Promise => { - try { - // First handle any initial responses - for (const response of responses) { - await callback(response.content); - } - - // Get pending tasks for this room - const pendingTasks = runtime.getTasks({ - roomId: message.roomId, - tags: ["AWAITING_CONFIRMATION"], - }); - - if (!pendingTasks || pendingTasks.length === 0) { - await callback({ - text: "No tasks currently awaiting confirmation.", - action: "CANCEL_TASK", - source: message.content.source, - }); - return; - } - - // Cancel each pending task - for (const task of pendingTasks) { - runtime.deleteTask(task.id); - } - } catch (error) { - logger.error("Error in cancel task handler:", error); - } - }, - - examples: [ - [ - { - user: "{{user1}}", - content: { - text: "Actually, don't post that tweet", - }, - }, - { - user: "{{user2}}", - content: { - text: "Task cancelled successfully.", - action: "CANCEL_TASK", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { - text: "Cancel", - }, - }, - { - user: "{{user2}}", - content: { - text: "Task cancelled successfully.", - action: "CANCEL_TASK", - }, - }, - ], - ] as ActionExample[][], -}; diff --git a/packages/core/src/actions/confirm.ts b/packages/core/src/actions/confirm.ts deleted file mode 100644 index 34ec49247e1..00000000000 --- a/packages/core/src/actions/confirm.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { logger } from "../logger"; -import { Action, ActionExample, HandlerCallback, IAgentRuntime, Memory, State } from "../types"; - -export const confirmTaskAction: Action = { - name: "CONFIRM_TASK", - similes: ["APPROVE_TASK", "EXECUTE_TASK", "PROCEED", "GO_AHEAD", "CONFIRM"], - description: - "Confirms and executes a pending task that's awaiting confirmation", - - validate: async ( - runtime: IAgentRuntime, - _message: Memory, - _state: State - ): Promise => { - // Get all tasks with AWAITING_CONFIRMATION tag - const pendingTasks = runtime.getTasks({}); - - // Only validate if there are pending tasks - return pendingTasks && pendingTasks.length > 0; - }, - - handler: async ( - runtime: IAgentRuntime, - message: Memory, - _state: State, - _options: any, - callback: HandlerCallback, - responses: Memory[] - ): Promise => { - try { - // First handle any initial responses - for (const response of responses) { - await callback(response.content); - } - - // Get pending tasks for this room - const pendingTasks = runtime.getTasks({ - roomId: message.roomId, - tags: ["AWAITING_CONFIRMATION"], - }); - - if (!pendingTasks || pendingTasks.length === 0) { - await callback({ - text: "No tasks currently awaiting confirmation.", - action: "CONFIRM_TASK", - source: message.content.source, - }); - return; - } - - // Process each pending task - for (const task of pendingTasks) { - try { - // Execute the task handler - await task.handler(runtime); - - // Delete the task after successful execution - runtime.deleteTask(task.id); - } catch (error) { - logger.error("Error executing task:", error); - await callback({ - text: "There was an error executing the task.", - action: "CONFIRM_TASK_ERROR", - source: message.content.source, - }); - } - } - } catch (error) { - logger.error("Error in confirm task handler:", error); - await callback({ - text: "There was an error processing the task confirmation.", - action: "CONFIRM_TASK_ERROR", - source: message.content.source, - }); - } - }, - - examples: [ - [ - { - user: "{{user1}}", - content: { - text: "Yes, go ahead and post that tweet", - }, - }, - { - user: "{{user2}}", - content: { - text: "Task confirmed and executed successfully!", - action: "CONFIRM_TASK", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { - text: "Confirm", - }, - }, - { - user: "{{user2}}", - content: { - text: "Task confirmed and executed successfully!", - action: "CONFIRM_TASK", - }, - }, - ], - ] as ActionExample[][], -}; diff --git a/packages/core/src/actions/followRoom.ts b/packages/core/src/actions/followRoom.ts index 9e57bb2380e..348dba166e7 100644 --- a/packages/core/src/actions/followRoom.ts +++ b/packages/core/src/actions/followRoom.ts @@ -1,7 +1,7 @@ import { composeContext } from "../context"; import { generateTrueOrFalse } from "../generation"; import { booleanFooter } from "../parsing"; -import { Action, ActionExample, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "../types"; +import { type Action, type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, type State } from "../types"; export const shouldFollowTemplate = `# Task: Decide if {{agentName}} should start following this room, i.e. eagerly participating without explicit mentions. diff --git a/packages/core/src/actions/ignore.ts b/packages/core/src/actions/ignore.ts index 9d5b1054339..3577bbc40a9 100644 --- a/packages/core/src/actions/ignore.ts +++ b/packages/core/src/actions/ignore.ts @@ -1,4 +1,4 @@ -import { Action, ActionExample, IAgentRuntime, Memory } from "../types"; +import type { Action, ActionExample, IAgentRuntime, Memory } from "../types"; export const ignoreAction: Action = { name: "IGNORE", @@ -109,7 +109,7 @@ export const ignoreAction: Action = { }, { user: "{{user1}}", - content: { text: "Yeah", action: "CONTINUE" }, + content: { text: "Yeah" }, }, { user: "{{user1}}", diff --git a/packages/core/src/actions/muteRoom.ts b/packages/core/src/actions/muteRoom.ts index 9ca386059a8..b20245b88ef 100644 --- a/packages/core/src/actions/muteRoom.ts +++ b/packages/core/src/actions/muteRoom.ts @@ -1,7 +1,7 @@ import { composeContext } from "../context"; import { generateTrueOrFalse } from "../generation"; import { booleanFooter } from "../parsing"; -import { Action, ActionExample, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "../types"; +import { type Action, type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, type State } from "../types"; export const shouldMuteTemplate = `# Task: Decide if {{agentName}} should mute this room and stop responding unless explicitly mentioned. diff --git a/packages/core/src/actions/none.ts b/packages/core/src/actions/none.ts index a71bad32332..af9179022ba 100644 --- a/packages/core/src/actions/none.ts +++ b/packages/core/src/actions/none.ts @@ -1,4 +1,4 @@ -import { Action, ActionExample, IAgentRuntime, Memory } from "../types"; +import type { Action, ActionExample, IAgentRuntime, Memory } from "../types"; export const noneAction: Action = { name: "NONE", diff --git a/packages/core/src/actions/options.ts b/packages/core/src/actions/options.ts new file mode 100644 index 00000000000..bebec867bce --- /dev/null +++ b/packages/core/src/actions/options.ts @@ -0,0 +1,233 @@ +import { composeContext } from "../context"; +import { logger } from "../logger"; +import { parseJSONObjectFromText } from "../parsing"; +import { + type Action, + type ActionExample, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type State, +} from "../types"; + +const optionExtractionTemplate = `# Task: Extract selected task and option from user message + +# Available Tasks: +{{#each tasks}} +Task {{taskId}}: {{name}} +Available options: +{{#each options}} +- {{name}}: {{description}} +{{/each}} +- ABORT: Cancel this task + +{{/each}} + +# Recent Messages: +{{recentMessages}} + +# Instructions: +1. Review the user's message and identify which task and option they are selecting +2. Match against the available tasks and their options, including ABORT +3. Return the task ID and selected option name exactly as listed above +4. If no clear selection is made, return null for both fields + +Return in JSON format: +\`\`\`json +{ + "taskId": number | null, + "selectedOption": "OPTION_NAME" | null +} +\`\`\` + +Make sure to include the \`\`\`json\`\`\` tags around the JSON object.`; + +export const selectOptionAction: Action = { + name: "SELECT_OPTION", + similes: ["CHOOSE_OPTION", "SELECT", "PICK", "CHOOSE"], + description: "Selects an option for a pending task that has multiple options", + + validate: async ( + runtime: IAgentRuntime, + message: Memory, + _state: State + ): Promise => { + // Get all tasks with options metadata + const pendingTasks = runtime.getTasks({ + roomId: message.roomId, + tags: ["AWAITING_CHOICE"], + }); + + // Only validate if there are pending tasks with options + return ( + pendingTasks && + pendingTasks.length > 0 && + pendingTasks.some((task) => task.metadata?.options) + ); + }, + + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: any, + callback: HandlerCallback, + responses: Memory[] + ): Promise => { + try { + // Handle initial responses + for (const response of responses) { + await callback(response.content); + } + + const pendingTasks = runtime.getTasks({ + roomId: message.roomId, + tags: ["AWAITING_CHOICE"], + }); + + if (!pendingTasks?.length) { + await callback({ + text: "No tasks currently awaiting options selection.", + action: "SELECT_OPTION", + source: message.content.source, + }); + return; + } + + const tasksWithOptions = pendingTasks.filter( + (task) => task.metadata?.options + ); + + if (!tasksWithOptions.length) { + await callback({ + text: "No tasks currently have options to select from.", + action: "SELECT_OPTION", + source: message.content.source, + }); + return; + } + + // Format tasks with their options for the LLM + const formattedTasks = tasksWithOptions.map((task, index) => ({ + taskId: index + 1, + name: task.name, + options: task.metadata.options.map(opt => ({ + name: typeof opt === 'string' ? opt : opt.name, + description: typeof opt === 'string' ? opt : opt.description || opt.name + })) + })); + + const context = composeContext({ + state: { + ...state, + tasks: formattedTasks, + recentMessages: message.content.text + }, + template: optionExtractionTemplate + }); + + const result = await runtime.useModel(ModelClass.TEXT_SMALL, { + context, + stopSequences: [] + }); + + const parsed = parseJSONObjectFromText(result); + const { taskId, selectedOption } = parsed; + + if (taskId && selectedOption) { + const selectedTask = tasksWithOptions[taskId - 1]; + + if (selectedOption === 'ABORT') { + runtime.deleteTask(selectedTask.id); + await callback({ + text: `Task "${selectedTask.name}" has been cancelled.`, + action: "SELECT_OPTION", + source: message.content.source, + }); + return; + } + + try { + await selectedTask.handler(runtime, { option: selectedOption }); + runtime.deleteTask(selectedTask.id); + await callback({ + text: `Selected option: ${selectedOption} for task: ${selectedTask.name}`, + action: "SELECT_OPTION", + source: message.content.source, + }); + return; + } catch (error) { + logger.error("Error executing task with option:", error); + await callback({ + text: "There was an error processing your selection.", + action: "SELECT_OPTION_ERROR", + source: message.content.source, + }); + return; + } + } + + // If no task/option was selected, list available options + let optionsText = "Please select a valid option from one of these tasks:\n\n"; + tasksWithOptions.forEach((task, index) => { + optionsText += `${index + 1}. **${task.name}**:\n`; + const options = task.metadata.options.map(opt => + typeof opt === 'string' ? opt : opt.name + ); + options.push('ABORT'); + optionsText += options.map(opt => `- ${opt}`).join('\n'); + optionsText += '\n\n'; + }); + + await callback({ + text: optionsText, + action: "SELECT_OPTION_INVALID", + source: message.content.source, + }); + + } catch (error) { + logger.error("Error in select option handler:", error); + await callback({ + text: "There was an error processing the option selection.", + action: "SELECT_OPTION_ERROR", + source: message.content.source, + }); + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "post", + }, + }, + { + user: "{{user2}}", + content: { + text: "Selected option: post for task: Confirm Twitter Post", + action: "SELECT_OPTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "I choose cancel", + }, + }, + { + user: "{{user2}}", + content: { + text: "Selected option: cancel for task: Confirm Twitter Post", + action: "SELECT_OPTION", + }, + }, + ], + ] as ActionExample[][], +}; + +export default selectOptionAction; diff --git a/packages/core/src/actions/roles.ts b/packages/core/src/actions/roles.ts index 11bd414c24a..d73f8b87c3d 100644 --- a/packages/core/src/actions/roles.ts +++ b/packages/core/src/actions/roles.ts @@ -1,8 +1,7 @@ -import { generateObjectArray } from ".."; +import { createUniqueUuid, generateObjectArray } from ".."; import { composeContext } from "../context"; import { logger } from "../logger"; -import { Action, ActionExample, ChannelType, HandlerCallback, IAgentRuntime, Memory, ModelClass, RoleName, State, UUID } from "../types"; -import { stringToUuid } from "../uuid"; +import { type Action, type ActionExample, ChannelType, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, RoleName, type State, type UUID } from "../types"; // Role modification validation helper const canModifyRole = ( @@ -70,7 +69,7 @@ const updateRoleAction: Action = { name: "UPDATE_ROLE", similes: ["CHANGE_ROLE", "SET_ROLE", "MODIFY_ROLE"], description: - "Updates the role for a user with respect to the agent, world being the server they are in. For example, if an admin tells the agent that a user is their boss, set their role to ADMIN.", + "Updates the role for a user with respect to the agent, world being the server they are in. For example, if an admin tells the agent that a user is their boss, set their role to ADMIN. Can only be used to set roles to ADMIN, OWNER or NONE. Can't be used to ban.", validate: async ( runtime: IAgentRuntime, @@ -102,20 +101,11 @@ const updateRoleAction: Action = { } try { // Get world data instead of ownership state from cache - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, serverId); const world = await runtime.getWorld(worldId); // Get requester ID and convert to UUID for consistent lookup const requesterId = message.userId; - const requesterUuid = stringToUuid(requesterId); - const requesterUuidCombined = stringToUuid(`${requesterId}-${runtime.agentId}`); - const tenantSpecificUserId = runtime.generateTenantUserId(requesterId); - - console.log('requesterId', requesterId); - console.log('requesterUuid', requesterUuid); - console.log('requesterUuidCombined', requesterUuidCombined); - console.log('tenantSpecificUserId', tenantSpecificUserId); - console.log("world.metadata.roles", world.metadata.roles) // Get roles from world metadata if (!world.metadata?.roles) { @@ -124,9 +114,9 @@ const updateRoleAction: Action = { } // Lookup using UUID for consistency - const requesterRole = world.metadata.roles[tenantSpecificUserId] as RoleName + const requesterRole = world.metadata.roles[requesterId] as RoleName - logger.info(`Requester ${tenantSpecificUserId} role:`, requesterRole); + logger.info(`Requester ${requesterId} role:`, requesterRole); if (!requesterRole) { logger.info("Validation failed: No requester role found"); @@ -161,18 +151,14 @@ const updateRoleAction: Action = { } const room = await runtime.getRoom(message.roomId); + const world = await runtime.getWorld(room.worldId); if (!room) { throw new Error("No room found"); } - const serverId = room.serverId; + const serverId = world.serverId; const requesterId = message.userId; - const tenantSpecificRequesterId = runtime.generateTenantUserId(requesterId); - - // Get world data instead of role cache - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); - let world = await runtime.getWorld(worldId); if (!world || !world.metadata) { logger.error(`No world or metadata found for server ${serverId}`); @@ -191,15 +177,19 @@ const updateRoleAction: Action = { // Get requester's role from world metadata const requesterRole = - (world.metadata.roles[tenantSpecificRequesterId] as RoleName) || RoleName.NONE; - - const discordClient = runtime.getClient("discord").client; - const guild = await discordClient.guilds.fetch(serverId); - - // Build server members context - const members = await guild.members.fetch(); - const serverMembersContext = Array.from(members.values()) - .map((member: any) => `${member.username} (${member.id})`) + (world.metadata.roles[requesterId] as RoleName) || RoleName.NONE; + + // Get all entities in the room + const entities = await runtime.databaseAdapter.getEntitiesForRoom(room.id, runtime.agentId, true); + + // Build server members context from entities + const serverMembersContext = entities + .map(entity => { + const discordData = entity.components?.find(c => c.type === 'discord')?.data; + const name = discordData?.username || entity.names[0]; + const id = entity.id; + return `${name} (${id})`; + }) .join("\n"); // Create extraction context @@ -220,7 +210,6 @@ const updateRoleAction: Action = { })) as RoleAssignment[]; if (!result?.length) { - console.log("No valid role assignments found in the request."); await callback({ text: "No valid role assignments found in the request.", action: "UPDATE_ROLE", @@ -233,16 +222,21 @@ const updateRoleAction: Action = { let worldUpdated = false; for (const assignment of result) { - const targetUser = members.get(assignment.userId); - if (!targetUser) continue; + let targetEntity = entities.find(e => e.id === assignment.userId); + if(!targetEntity) { + targetEntity = entities.find(e => e.id === assignment.userId); + console.log("Trying to write to generated tenant ID") + } + if (!targetEntity) { + console.log("Could not find an ID ot assign to") + } - const tenantSpecificTargetId = runtime.generateTenantUserId(assignment.userId); - const currentRole = world.metadata.roles[tenantSpecificTargetId]; + const currentRole = world.metadata.roles[assignment.userId]; // Validate role modification permissions if (!canModifyRole(requesterRole, currentRole, assignment.newRole)) { await callback({ - text: `You don't have permission to change ${targetUser.user.username}'s role to ${assignment.newRole}.`, + text: `You don't have permission to change ${targetEntity.names[0]}'s role to ${assignment.newRole}.`, action: "UPDATE_ROLE", source: "discord", }); @@ -250,12 +244,12 @@ const updateRoleAction: Action = { } // Update role in world metadata - world.metadata.roles[tenantSpecificTargetId] = assignment.newRole; + world.metadata.roles[assignment.userId] = assignment.newRole; worldUpdated = true; await callback({ - text: `Updated ${targetUser.user.username}'s role to ${assignment.newRole}.`, + text: `Updated ${targetEntity.names[0]}'s role to ${assignment.newRole}.`, action: "UPDATE_ROLE", source: "discord", }); @@ -301,6 +295,22 @@ const updateRoleAction: Action = { }, }, ], + [ + { + user: "{{user1}}", + content: { + text: "Ban @troublemaker", + source: "discord", + } + }, + { + user: "{{user3}}", + content: { + text: "I cannot ban users.", + action: "REPLY", + } + } + ] ] as ActionExample[][], }; diff --git a/packages/core/src/actions/sendMessage.ts b/packages/core/src/actions/sendMessage.ts new file mode 100644 index 00000000000..1218164eb01 --- /dev/null +++ b/packages/core/src/actions/sendMessage.ts @@ -0,0 +1,296 @@ +// action: SEND_MESSAGE +// send message to a user or room (other than this room we are in) + +import { v4 as uuidv4 } from 'uuid'; +import { logger } from "../logger"; +import { + type Action, + type ActionExample, + Component, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type State, + UUID, + Entity, + sendDirectMessage, + sendRoomMessage +} from "../types"; +import { composeContext } from "../context"; +import { findEntityByName } from "../entities"; +import { parseJSONObjectFromText } from "../parsing"; + +const targetExtractionTemplate = `# Task: Extract Target and Source Information + +# Recent Messages: +{{recentMessages}} + +# Instructions: +Analyze the conversation to identify: +1. The target type (user or room) +2. The target platform/source (e.g. telegram, discord, etc) +3. Any identifying information about the target + +Return a JSON object with: +\`\`\`json +{ + "targetType": "user|room", + "source": "platform-name", + "identifiers": { + // Relevant identifiers for that target + // e.g. username, roomName, etc. + } +} +\`\`\` +Example outputs: +1. For "send a message to @dev_guru on telegram": +\`\`\`json +{ + "targetType": "user", + "source": "telegram", + "identifiers": { + "username": "dev_guru" + } +} +\`\`\` + +2. For "post this in #announcements": +\`\`\`json +{ + "targetType": "room", + "source": "discord", + "identifiers": { + "roomName": "announcements" + } +} +\`\`\` + +Make sure to include the \`\`\`json\`\`\` tags around the JSON object.`; + +export const sendMessageAction: Action = { + name: "SEND_MESSAGE", + similes: ["DM", "MESSAGE", "SEND_DM", "POST_MESSAGE"], + description: "Send a message to a user or room (other than the current one)", + + validate: async ( + runtime: IAgentRuntime, + message: Memory, + _state: State + ): Promise => { + // Check if we have permission to send messages + const worldId = message.roomId; + const agentId = runtime.agentId; + + // Get all components for the current room to understand available sources + const roomComponents = await runtime.databaseAdapter.getComponents(message.roomId, worldId, agentId); + + // Get source types from room components + const availableSources = new Set(roomComponents.map(c => c.type)); + + // TODO: Add ability for plugins to register their sources + // const registeredSources = runtime.getRegisteredSources?.() || []; + // availableSources.add(...registeredSources); + + return availableSources.size > 0; + }, + + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: any, + callback: HandlerCallback, + responses: Memory[] + ): Promise => { + try { + // Handle initial responses + for (const response of responses) { + await callback(response.content); + } + + const sourceEntityId = message.userId; + const roomId = message.roomId; + const _agentId = runtime.agentId; + const room = await runtime.getRoom(roomId); + const worldId = room.worldId; + + // Extract target and source information + const targetContext = composeContext({ + state, + template: targetExtractionTemplate, + }); + + const targetResult = await runtime.useModel(ModelClass.TEXT_LARGE, { + context: targetContext, + stopSequences: [] + }); + + const targetData = parseJSONObjectFromText(targetResult); + if (!targetData?.targetType || !targetData?.source) { + await callback({ + text: "I couldn't determine where you want me to send the message. Could you please specify the target (user or room) and platform?", + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + return; + } + + const source = targetData.source.toLowerCase(); + + if (targetData.targetType === "user") { + // Try to find the target user entity + const targetEntity = await findEntityByName(runtime, message, state); + + if (!targetEntity) { + await callback({ + text: "I couldn't find the user you want me to send a message to. Could you please provide more details about who they are?", + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + return; + } + + // Get the component for the specified source + const userComponent = await runtime.databaseAdapter.getComponent( + targetEntity.id!, + source, + worldId, + sourceEntityId + ); + + if (!userComponent) { + await callback({ + text: `I couldn't find ${source} information for that user. Could you please provide their ${source} details?`, + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + return; + } + + // Send the message using the appropriate client + try { + await sendDirectMessage( + runtime, + targetEntity.id!, + source, + message.content.text, + worldId + ); + + await callback({ + text: `Message sent to ${targetEntity.names[0]} on ${source}.`, + action: "SEND_MESSAGE", + source: message.content.source, + }); + } catch (error) { + logger.error(`Failed to send direct message: ${error.message}`); + await callback({ + text: "I encountered an error trying to send the message. Please try again.", + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + } + + } else if (targetData.targetType === "room") { + // Try to find the target room + const rooms = await runtime.databaseAdapter.getRooms(worldId); + const targetRoom = rooms.find(r => { + // Match room name from identifiers + return r.name.toLowerCase() === targetData.identifiers.roomName?.toLowerCase(); + }); + + if (!targetRoom) { + await callback({ + text: "I couldn't find the room you want me to send a message to. Could you please specify the exact room name?", + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + return; + } + + // Send the message to the room + try { + await sendRoomMessage( + runtime, + targetRoom.id, + source, + message.content.text, + worldId + ); + + await callback({ + text: `Message sent to ${targetRoom.name} on ${source}.`, + action: "SEND_MESSAGE", + source: message.content.source, + }); + } catch (error) { + logger.error(`Failed to send room message: ${error.message}`); + await callback({ + text: "I encountered an error trying to send the message to the room. Please try again.", + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + } + } + + } catch (error) { + logger.error(`Error in sendMessage handler: ${error}`); + await callback({ + text: "There was an error processing your message request.", + action: "SEND_MESSAGE_ERROR", + source: message.content.source, + }); + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Send a message to @dev_guru on telegram saying 'Hello!'", + }, + }, + { + user: "{{user2}}", + content: { + text: "Message sent to dev_guru on telegram.", + action: "SEND_MESSAGE", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Post 'Important announcement!' in #announcements", + }, + }, + { + user: "{{user2}}", + content: { + text: "Message sent to announcements.", + action: "SEND_MESSAGE", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "DM Jimmy and tell him 'Meeting at 3pm'", + }, + }, + { + user: "{{user2}}", + content: { + text: "Message sent to Jimmy.", + action: "SEND_MESSAGE", + }, + }, + ], + ] as ActionExample[][], +}; + +export default sendMessageAction; \ No newline at end of file diff --git a/packages/core/src/actions/settings.ts b/packages/core/src/actions/settings.ts index 95693691587..4718d82521c 100644 --- a/packages/core/src/actions/settings.ts +++ b/packages/core/src/actions/settings.ts @@ -1,21 +1,21 @@ import { composeContext } from "../context"; +import { createUniqueUuid } from "../entities"; import { generateMessageResponse, generateObjectArray } from "../generation"; import { logger } from "../logger"; import { messageCompletionFooter } from "../parsing"; -import { findWorldForOwner, normalizeUserId } from "../roles"; +import { findWorldForOwner } from "../roles"; import { - Action, - ActionExample, - ChannelType, - HandlerCallback, - IAgentRuntime, - Memory, - ModelClass, - OnboardingSetting, - WorldSettings, - State, + type Action, + type ActionExample, + ChannelType, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type OnboardingSetting, + type State, + type WorldSettings, } from "../types"; -import { stringToUuid } from "../uuid"; interface SettingUpdate { key: string; @@ -63,7 +63,7 @@ export async function getWorldSettings( serverId: string ): Promise { try { - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, serverId); const world = await runtime.getWorld(worldId); if (!world || !world.metadata?.settings) { @@ -86,7 +86,7 @@ export async function updateWorldSettings( worldSettings: WorldSettings ): Promise { try { - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, serverId); const world = await runtime.getWorld(worldId); if (!world) { @@ -164,7 +164,7 @@ function categorizeSettings(worldSettings: WorldSettings): { */ async function extractSettingValues( runtime: IAgentRuntime, - message: Memory, + _message: Memory, state: State, worldSettings: WorldSettings ): Promise { @@ -328,7 +328,7 @@ const successTemplate = `# Task: Generate a response for successful setting upda Write a natural, conversational response that {{agentName}} would send about the successful update and next steps. Include the action "SETTING_UPDATED" in your response. -` + messageCompletionFooter; +${messageCompletionFooter}`; // Template for failure responses when settings couldn't be updated const failureTemplate = `# Task: Generate a response for failed setting updates @@ -357,7 +357,7 @@ const failureTemplate = `# Task: Generate a response for failed setting updates Write a natural, conversational response that {{agentName}} would send about the failed update and how to proceed. Include the action "SETTING_UPDATE_FAILED" in your response. -` + messageCompletionFooter; +${messageCompletionFooter}`; // Template for error responses when unexpected errors occur const errorTemplate = `# Task: Generate a response for an error during setting updates @@ -376,7 +376,7 @@ const errorTemplate = `# Task: Generate a response for an error during setting u Write a natural, conversational response that {{agentName}} would send about the error. Include the action "SETTING_UPDATE_ERROR" in your response. -` + messageCompletionFooter; +${messageCompletionFooter}`; // Template for completion responses when all required settings are configured const completionTemplate = `# Task: Generate a response for settings completion @@ -400,7 +400,7 @@ const completionTemplate = `# Task: Generate a response for settings completion Write a natural, conversational response that {{agentName}} would send about the successful completion of settings. Include the action "ONBOARDING_COMPLETE" in your response. -` + messageCompletionFooter; +${messageCompletionFooter}`; /** * Handles the completion of settings when all required settings are configured @@ -600,9 +600,8 @@ const updateSettingsAction: Action = { } // Log the user ID for debugging - const normalizedUserId = normalizeUserId(message.userId); logger.info( - `Validating settings action for user ${message.userId} (normalized: ${normalizedUserId})` + `Validating settings action for user ${message.userId} (normalized: ${message.userId})` ); // Validate that we're in a DM channel @@ -730,7 +729,7 @@ const updateSettingsAction: Action = { serverId ); if (!updatedWorldSettings) { - logger.error(`Failed to retrieve updated settings state`); + logger.error("Failed to retrieve updated settings state"); await generateErrorResponse(runtime, state, callback); return; } diff --git a/packages/core/src/actions/unfollowRoom.ts b/packages/core/src/actions/unfollowRoom.ts index 305c11ac5f5..bf76be988a8 100644 --- a/packages/core/src/actions/unfollowRoom.ts +++ b/packages/core/src/actions/unfollowRoom.ts @@ -1,7 +1,7 @@ import { generateTrueOrFalse } from ".."; import { composeContext } from "../context"; import { booleanFooter } from "../parsing"; -import { Action, ActionExample, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "../types"; +import { type Action, type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, type State } from "../types"; const shouldUnfollowTemplate = `# Task: Decide if {{agentName}} should stop closely following this previously followed room and only respond when mentioned. diff --git a/packages/core/src/actions/unmuteRoom.ts b/packages/core/src/actions/unmuteRoom.ts index 13dd31017c6..14d82dc5f74 100644 --- a/packages/core/src/actions/unmuteRoom.ts +++ b/packages/core/src/actions/unmuteRoom.ts @@ -1,7 +1,7 @@ import { generateTrueOrFalse } from "../generation"; import { composeContext } from "../context"; import { booleanFooter } from "../parsing"; -import { Action, ActionExample, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "../types"; +import { type Action, type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, type State } from "../types"; export const shouldUnmuteTemplate = `# Task: Decide if {{agentName}} should unmute this previously muted room and start considering it for responses again. diff --git a/packages/core/src/actions/updateEntity.ts b/packages/core/src/actions/updateEntity.ts new file mode 100644 index 00000000000..6a52a387931 --- /dev/null +++ b/packages/core/src/actions/updateEntity.ts @@ -0,0 +1,285 @@ +// I want to create an action that lets anyone create or update a component for an entity. +// Components represent different sources of data about an entity (telegram, twitter, etc) +// Sources can be registered by plugins or inferred from room context and available components +// The action should first check if the component exists for the entity, and if not, create it. +// We want to use an LLM (runtime.useModel) to generate the component data. +// We should include the prior component data if it exists, and have the LLM output an update to the component. +// sourceEntityId represents who is making the update, entityId is who they are talking about + +import { v4 as uuidv4 } from 'uuid'; +import { composeContext } from "../context"; +import { findEntityByName } from "../entities"; +import { logger } from "../logger"; +import { parseJSONObjectFromText } from "../parsing"; +import { + type Action, + type ActionExample, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type State, + type UUID +} from "../types"; + +const componentTemplate = `# Task: Extract Source and Update Component Data + +{{recentMessages}} + +{{#if existingData}} +# Existing Component Data: +\`\`\`json +{{existingData}} +\`\`\` +{{/if}} + +# Instructions: +1. Analyze the conversation to identify: + - The source/platform being referenced (e.g. telegram, twitter, discord) + - Any specific component data being shared + +2. Generate updated component data that: + - Is specific to the identified platform/source + - Preserves existing data when appropriate + - Includes the new information from the conversation + - Contains only valid data for this component type + +Return a JSON object with the following structure: +\`\`\`json +{ + "source": "platform-name", + "data": { + // Component-specific fields + // e.g. username, username, displayName, etc. + } +} +\`\`\` + +Example outputs: +1. For "my telegram username is @dev_guru": +\`\`\`json +{ + "source": "telegram", + "data": { + "username": "dev_guru" + } +} +\`\`\` + +2. For "update my twitter handle to @tech_master": +\`\`\`json +{ + "source": "twitter", + "data": { + "username": "tech_master" + } +} +\`\`\` + +Make sure to include the \`\`\`json\`\`\` tags around the JSON object.`; + +export const updateEntityAction: Action = { + name: "UPDATE_ENTITY", + similes: ["CREATE_ENTITY", "UPDATE_USER", "EDIT_ENTITY", "UPDATE_COMPONENT", "CREATE_COMPONENT"], + description: "Add or edit contact details for a user entity (like twitter, discord, email address, etc.)", + + validate: async ( + runtime: IAgentRuntime, + message: Memory, + _state: State + ): Promise => { + // Check if we have any registered sources or existing components that could be updated + // const worldId = message.roomId; + // const agentId = runtime.agentId; + + // // Get all components for the current room to understand available sources + // const roomComponents = await runtime.databaseAdapter.getComponents(message.roomId, worldId, agentId); + + // // Get source types from room components + // const availableSources = new Set(roomComponents.map(c => c.type)); + + // console.log("*** updateEntityAction validate:", availableSources.size > 0) + return true; // availableSources.size > 0; + }, + + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: any, + callback: HandlerCallback, + responses: Memory[] + ): Promise => { + console.log('*** updateEntityAction handler') + try { + // Handle initial responses + for (const response of responses) { + await callback(response.content); + } + + const sourceEntityId = message.userId; + const roomId = message.roomId; + const agentId = runtime.agentId; + const room = await runtime.getRoom(roomId); + const worldId = room.worldId; + + // First, find the entity being referenced + const entity = await findEntityByName(runtime, message, state); + + if (!entity) { + await callback({ + text: "I'm not sure which entity you're trying to update. Could you please specify who you're talking about?", + action: "UPDATE_ENTITY_ERROR", + source: message.content.source, + }); + return; + } + + // Get existing component if it exists - we'll get this after the LLM identifies the source + let existingComponent = null; + + // Generate component data using the combined template + const context = composeContext({ + state, + template: componentTemplate, + }); + + console.log("*** updateEntityAction context", context); + + const result = await runtime.useModel(ModelClass.TEXT_LARGE, { + context, + stopSequences: [] + }); + + console.log("*** updateEntityAction result", result); + + // Parse the generated data + let parsedResult: any; + try { + const jsonMatch = result.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error("No valid JSON found in the LLM response"); + } + + parsedResult = JSON.parse(jsonMatch[0]); + + if (!parsedResult.source || !parsedResult.data) { + throw new Error("Invalid response format - missing source or data"); + } + } catch (error) { + logger.error(`Failed to parse component data: ${error.message}`); + await callback({ + text: "I couldn't properly understand the component information. Please try again with more specific information.", + action: "UPDATE_ENTITY_ERROR", + source: message.content.source, + }); + return; + } + + const componentType = parsedResult.source.toLowerCase(); + const componentData = parsedResult.data; + + // Now that we know the component type, get the existing component if it exists + existingComponent = await runtime.databaseAdapter.getComponent( + entity.id!, + componentType, + worldId, + sourceEntityId + ); + + // Create or update the component + if (existingComponent) { + await runtime.databaseAdapter.updateComponent({ + id: existingComponent.id, + entityId: entity.id!, + worldId, + type: componentType, + data: componentData, + agentId, + roomId: message.roomId, + sourceEntityId + }); + + await callback({ + text: `I've updated the ${componentType} information for ${entity.names[0]}.`, + action: "UPDATE_ENTITY", + source: message.content.source, + }); + } else { + await runtime.databaseAdapter.createComponent({ + id: uuidv4() as UUID, + entityId: entity.id!, + worldId, + type: componentType, + data: componentData, + agentId, + roomId: message.roomId, + sourceEntityId + }); + + await callback({ + text: `I've added new ${componentType} information for ${entity.names[0]}.`, + action: "UPDATE_ENTITY", + source: message.content.source, + }); + } + } catch (error) { + logger.error(`Error in updateEntity handler: ${error}`); + await callback({ + text: "There was an error processing the entity information.", + action: "UPDATE_ENTITY_ERROR", + source: message.content.source, + }); + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Please update my telegram username to @dev_guru", + }, + }, + { + user: "{{user2}}", + content: { + text: "I've updated your telegram information.", + action: "UPDATE_ENTITY", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Set Jimmy's twitter username to @jimmy_codes", + }, + }, + { + user: "{{user2}}", + content: { + text: "I've updated Jimmy's twitter information.", + action: "UPDATE_ENTITY", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Update my discord username to dev_guru#1234", + }, + }, + { + user: "{{user2}}", + content: { + text: "I've updated your discord information.", + action: "UPDATE_ENTITY", + }, + }, + ], + ] as ActionExample[][], +}; + +export default updateEntityAction; \ No newline at end of file diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index 212b45726a4..ed345ef8d7e 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -1,46 +1,47 @@ -import { UUID } from "crypto"; +import type { UUID } from "node:crypto"; import { v4 } from "uuid"; -import { cancelTaskAction } from "./actions/cancel.ts"; -import { confirmTaskAction } from "./actions/confirm.ts"; import { followRoomAction } from "./actions/followRoom.ts"; import { ignoreAction } from "./actions/ignore.ts"; import { muteRoomAction } from "./actions/muteRoom.ts"; import { noneAction } from "./actions/none.ts"; +import { selectOptionAction } from "./actions/options.ts"; import updateRoleAction from "./actions/roles.ts"; +import { sendMessageAction } from "./actions/sendMessage.ts"; import updateSettingsAction from "./actions/settings.ts"; import { unfollowRoomAction } from "./actions/unfollowRoom.ts"; import { unmuteRoomAction } from "./actions/unmuteRoom.ts"; +import { updateEntityAction } from "./actions/updateEntity.ts"; import { composeContext } from "./context.ts"; -import { factEvaluator } from "./evaluators/fact.ts"; import { goalEvaluator } from "./evaluators/goal.ts"; +import { reflectionEvaluator } from "./evaluators/reflection.ts"; import { - formatActors, formatMessages, generateMessageResponse, generateShouldRespond, - getActorDetails, + getActorDetails } from "./index.ts"; import { logger } from "./logger.ts"; import { messageCompletionFooter, shouldRespondFooter } from "./parsing.ts"; -import { confirmationTasksProvider } from "./providers/confirmation.ts"; import { factsProvider } from "./providers/facts.ts"; +import { optionsProvider } from "./providers/options.ts"; +import { relationshipsProvider } from "./providers/relationships.ts"; import { roleProvider } from "./providers/roles.ts"; import { settingsProvider } from "./providers/settings.ts"; import { timeProvider } from "./providers/time.ts"; import { ChannelType, - Entity, - HandlerCallback, - IAgentRuntime, - Memory, + type Entity, + type HandlerCallback, + type IAgentRuntime, + type Memory, ModelClass, - Plugin, + type Plugin, RoleName, - RoomData, - State, - WorldData, + type RoomData, + type State, + type WorldData, } from "./types.ts"; -import { stringToUuid } from "./uuid.ts"; +import { createUniqueUuid } from "./entities.ts"; type ServerJoinedParams = { runtime: IAgentRuntime; @@ -69,6 +70,8 @@ type UserJoinedParams = { export const shouldRespondTemplate = `{{system}} # Task: Decide on behalf of {{agentName}} whether they should respond to the message, ignore it or stop the conversation. +{{actors}} + About {{agentName}}: {{bio}} @@ -77,15 +80,16 @@ About {{agentName}}: # INSTRUCTIONS: Respond with the word RESPOND if {{agentName}} should respond to the message. Respond with STOP if a user asks {{agentName}} to be quiet. Respond with IGNORE if {{agentName}} should ignore the message. ${shouldRespondFooter}`; -const messageHandlerTemplate = `# Task: Generate dialog and actions for the character {{agentName}}. +export const messageHandlerTemplate = `# Task: Generate dialog and actions for the character {{agentName}}. {{system}} {{actionExamples}} (Action examples are for reference only. Do not use the information from them in your response.) -# Knowledge {{knowledge}} +{{actors}} + About {{agentName}}: {{bio}} @@ -233,11 +237,13 @@ const messageReceivedHandler = async ({ runtime.character.templates?.messageHandlerTemplate || messageHandlerTemplate, }); + console.log('*** context', context) const responseContent = await generateMessageResponse({ runtime: runtime, context, modelClass: ModelClass.TEXT_LARGE, }); + console.log('*** responseContent', responseContent) // Check if this is still the latest response ID for this agent+room const currentResponseId = agentResponses.get(message.roomId); @@ -249,9 +255,7 @@ const messageReceivedHandler = async ({ } responseContent.text = responseContent.text?.trim(); - responseContent.inReplyTo = stringToUuid( - `${message.id}-${runtime.agentId}` - ); + responseContent.inReplyTo = createUniqueUuid(runtime, message.id); const responseMessages: Memory[] = [ { @@ -308,9 +312,8 @@ const syncServerUsers = async ( try { // Create/ensure the world exists for this server - const worldId = stringToUuid(`${server.id}-${runtime.agentId}`); - - const ownerId = stringToUuid(`${server.ownerId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, server.id); + const ownerId = createUniqueUuid(runtime, server.ownerId); await runtime.ensureWorldExists({ id: worldId, @@ -473,14 +476,14 @@ const syncServerChannels = async ( try { if (source === "discord") { const guild = await server.fetch(); - const worldId = stringToUuid(`${guild.id}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, guild.id); // Loop through all channels and create room entities for (const [channelId, channel] of guild.channels.cache) { // Only process text and voice channels if (channel.type === 0 || channel.type === 2) { // GUILD_TEXT or GUILD_VOICE - const roomId = stringToUuid(`${channelId}-${runtime.agentId}`); + const roomId = createUniqueUuid(runtime, channelId); const room = await runtime.getRoom(roomId); // Skip if room already exists @@ -600,13 +603,13 @@ const syncSingleUser = async ( return; } - const roomId = stringToUuid(`${channelId}-${runtime.agentId}`); - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const roomId = createUniqueUuid(runtime, channelId); + const worldId = createUniqueUuid(runtime, serverId); await runtime.ensureConnection({ userId: user.id, roomId, - userName: user.username || `User${user.id}`, + userName: user.username || user.displayName || `User${user.id}`, userScreenName: user.displayName || user.username || `User${user.id}`, source, channelId, @@ -670,30 +673,24 @@ const handleServerSync = async ({ for (let i = 0; i < users.length; i += batchSize) { const userBatch = users.slice(i, i + batchSize); - // Find a default text channel for these users if possible - const defaultRoom = - rooms.find( - (room) => - room.type === ChannelType.GROUP && room.name.includes("general") - ) || rooms.find((room) => room.type === ChannelType.GROUP); - - if (defaultRoom) { - // Process each user in the batch - await Promise.all( - userBatch.map(async (user: Entity) => { + // check if user is in any of these rooms in rooms + const firstRoomUserIsIn = rooms.length > 0 ? rooms[0] : null; + + // Process each user in the batch + await Promise.all( + userBatch.map(async (user: Entity) => { try { await runtime.ensureConnection({ userId: user.id, - roomId: defaultRoom.id, + roomId: firstRoomUserIsIn.id, userName: - user.metadata[source].username || - user.metadata.default.username, + user.metadata[source].username, userScreenName: - user.metadata[source].name || user.metadata.default.name, + user.metadata[source].name, source: source, - channelId: defaultRoom.channelId, + channelId: firstRoomUserIsIn.channelId, serverId: world.serverId, - type: defaultRoom.type, + type: firstRoomUserIsIn.type, worldId: world.id, }); } catch (err) { @@ -703,7 +700,6 @@ const handleServerSync = async ({ } }) ); - } // Add a small delay between batches if not the last batch if (i + batchSize < users.length) { @@ -736,15 +732,15 @@ const syncMultipleUsers = async ( source: string ) => { if (!channelId) { - logger.warn(`Cannot sync users without a valid channelId`); + logger.warn("Cannot sync users without a valid channelId"); return; } logger.info(`Syncing ${users.length} users for channel ${channelId}`); try { - const roomId = stringToUuid(`${channelId}-${runtime.agentId}`); - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const roomId = createUniqueUuid(runtime, channelId); + const worldId = createUniqueUuid(runtime, serverId); // Process users in batches to avoid overwhelming the system const batchSize = 10; for (let i = 0; i < users.length; i += batchSize) { @@ -848,19 +844,21 @@ export const bootstrapPlugin: Plugin = { noneAction, muteRoomAction, unmuteRoomAction, - cancelTaskAction, - confirmTaskAction, + sendMessageAction, + updateEntityAction, + selectOptionAction, updateRoleAction, updateSettingsAction, ], events, - evaluators: [factEvaluator, goalEvaluator], + evaluators: [reflectionEvaluator, goalEvaluator], providers: [ timeProvider, factsProvider, - confirmationTasksProvider, + optionsProvider, roleProvider, settingsProvider, + relationshipsProvider, ], }; diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index d8b4e3b4b4c..e0d806c628c 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -12,7 +12,9 @@ import type { RoomData, UUID, WorldData, - Agent + Agent, + Component, + Room } from "./types.ts"; /** @@ -44,6 +46,8 @@ export abstract class DatabaseAdapter implements IDatabaseAdapter { */ abstract getEntityById(userId: UUID, agentId: UUID): Promise; + abstract getEntitiesForRoom(roomId: UUID, agentId: UUID, includeComponents?: boolean): Promise; + abstract getAgent(agentId: UUID): Promise; abstract createAgent(agent: Agent): Promise; @@ -64,6 +68,46 @@ export abstract class DatabaseAdapter implements IDatabaseAdapter { */ abstract updateEntity(entity: Entity): Promise; + /** + * Retrieves a single component by entity ID and type. + * @param entityId The UUID of the entity the component belongs to + * @param type The type identifier for the component + * @param worldId Optional UUID of the world the component belongs to + * @param sourceEntityId Optional UUID of the source entity + * @returns Promise resolving to the Component if found, null otherwise + */ + abstract getComponent(entityId: UUID, type: string, worldId?: UUID, sourceEntityId?: UUID): Promise; + + /** + * Retrieves all components for an entity. + * @param entityId The UUID of the entity to get components for + * @param worldId Optional UUID of the world to filter components by + * @param sourceEntityId Optional UUID of the source entity to filter by + * @returns Promise resolving to array of Component objects + */ + abstract getComponents(entityId: UUID, worldId?: UUID, sourceEntityId?: UUID): Promise; + + /** + * Creates a new component in the database. + * @param component The component object to create + * @returns Promise resolving to true if creation was successful + */ + abstract createComponent(component: Component): Promise; + + /** + * Updates an existing component in the database. + * @param component The component object with updated properties + * @returns Promise that resolves when the update is complete + */ + abstract updateComponent(component: Component): Promise; + + /** + * Deletes a component from the database. + * @param componentId The UUID of the component to delete + * @returns Promise that resolves when the deletion is complete + */ + abstract deleteComponent(componentId: UUID): Promise; + /** * Retrieves memories based on the specified parameters. * @param params An object containing parameters for the memory retrieval. @@ -135,13 +179,6 @@ export abstract class DatabaseAdapter implements IDatabaseAdapter { type: string; }): Promise; - /** - * Retrieves details of actors in a given room. - * @param params An object containing the roomId to search for actors. - * @returns A Promise that resolves to an array of Actor objects. - */ - abstract getActorDetails(params: { roomId: UUID, agentId: UUID }): Promise; - /** * Searches for memories based on embeddings and other specified parameters. * @param params An object containing parameters for the memory search. @@ -178,7 +215,7 @@ export abstract class DatabaseAdapter implements IDatabaseAdapter { memory: Memory, tableName: string, unique?: boolean - ): Promise; + ): Promise; /** * Removes a specific memory from the database. @@ -292,6 +329,13 @@ export abstract class DatabaseAdapter implements IDatabaseAdapter { */ abstract getRoom(roomId: UUID, agentId: UUID): Promise; + /** + * Retrieves all rooms for a given world. + * @param worldId The UUID of the world to retrieve rooms for. + * @returns A Promise that resolves to an array of Room objects. + */ + abstract getRooms(worldId: UUID): Promise; + /** * Creates a new room with an optional specified ID. * @param roomId Optional UUID to assign to the new room. @@ -372,40 +416,58 @@ export abstract class DatabaseAdapter implements IDatabaseAdapter { /** * Creates a new relationship between two users. - * @param params An object containing the UUIDs of the two users (userA and userB). + * @param params Object containing the relationship details including entity IDs, agent ID, optional tags and metadata * @returns A Promise that resolves to a boolean indicating success or failure of the creation. */ abstract createRelationship(params: { - userA: UUID; - userB: UUID; + sourceEntityId: UUID; + targetEntityId: UUID; + agentId: UUID; + tags?: string[]; + metadata?: { [key: string]: any }; }): Promise; /** * Retrieves a relationship between two users if it exists. - * @param params An object containing the UUIDs of the two users (userA and userB). + * @param params Object containing the entity IDs and agent ID * @returns A Promise that resolves to the Relationship object or null if not found. */ abstract getRelationship(params: { - userA: UUID; - userB: UUID; + sourceEntityId: UUID; + targetEntityId: UUID; + agentId: UUID; }): Promise; /** * Retrieves all relationships for a specific user. - * @param params An object containing the UUID of the user. + * @param params Object containing the user ID, agent ID and optional tags to filter by * @returns A Promise that resolves to an array of Relationship objects. */ abstract getRelationships(params: { userId: UUID; + agentId: UUID; + tags?: string[]; }): Promise; + /** + * Updates an existing relationship between two users. + * @param params Object containing the relationship details to update including entity IDs, agent ID, optional tags and metadata + * @returns A Promise that resolves to a boolean indicating success or failure of the update. + */ + abstract updateRelationship(params: { + sourceEntityId: UUID; + targetEntityId: UUID; + agentId: UUID; + tags?: string[]; + metadata?: { [key: string]: any }; + }): Promise; /** * Creates a new character in the database. * @param character The Character object to create. * @returns A Promise that resolves when the character creation is complete. */ - abstract createCharacter(character: Character): Promise; + abstract createCharacter(character: Character): Promise; /** * Retrieves all characters from the database. diff --git a/packages/core/src/entities.ts b/packages/core/src/entities.ts new file mode 100644 index 00000000000..5351c6644ed --- /dev/null +++ b/packages/core/src/entities.ts @@ -0,0 +1,265 @@ +import { composeContext } from "./context.ts"; +import { logger, stringToUuid } from "./index.ts"; +import { parseJSONObjectFromText } from "./parsing"; +import { + type Entity, + type IAgentRuntime, + type Memory, + ModelClass, + type State, + type UUID, + type Relationship +} from "./types.ts"; + +const entityResolutionTemplate = `# Task: Resolve Entity Name +Message Sender: {{senderName}} (ID: {{senderId}}) +Agent: {{agentName}} (ID: {{agentId}}) + +# Entities in Room: +{{#if entitiesInRoom}} +{{entitiesInRoom}} +{{/if}} + +{{recentMessages}} + +# Instructions: +1. Analyze the context to identify which entity is being referenced +2. Consider special references like "me" (the message sender) or "you" (agent the message is directed to) +3. Look for usernames/handles in standard formats (e.g. @username, user#1234) +4. Consider context from recent messages for pronouns and references +5. If multiple matches exist, use context to disambiguate +6. Consider recent interactions and relationship strength when resolving ambiguity + +Return a JSON object with: +\`\`\`json +{ + "entityId": "exact-id-if-known-otherwise-null", + "type": "EXACT_MATCH | USERNAME_MATCH | NAME_MATCH | RELATIONSHIP_MATCH | AMBIGUOUS | UNKNOWN", + "matches": [{ + "name": "matched-name", + "reason": "why this entity matches" + }] +} +\`\`\` + +Make sure to include the \`\`\`json\`\`\` tags around the JSON object. +`; + +async function getRecentInteractions( + runtime: IAgentRuntime, + sourceEntityId: UUID, + candidateEntities: Entity[], + roomId: UUID, + relationships: Relationship[] +): Promise<{ entity: Entity; interactions: Memory[]; count: number }[]> { + const results = []; + + // Get recent messages from the room - just for context + const recentMessages = await runtime.messageManager.getMemories({ + roomId, + count: 20 // Reduced from 100 since we only need context + }); + + for (const entity of candidateEntities) { + const interactions: Memory[] = []; + let interactionScore = 0; + + // First get direct replies using inReplyTo + const directReplies = recentMessages.filter(msg => + (msg.userId === sourceEntityId && msg.content.inReplyTo === entity.id) || + (msg.userId === entity.id && msg.content.inReplyTo === sourceEntityId) + ); + + interactions.push(...directReplies); + + // Get relationship strength from metadata + const relationship = relationships.find(rel => + (rel.sourceEntityId === sourceEntityId && rel.targetEntityId === entity.id) || + (rel.targetEntityId === sourceEntityId && rel.sourceEntityId === entity.id) + ); + + if (relationship?.metadata?.interactions) { + interactionScore = relationship.metadata.interactions; + } + + // Add bonus points for recent direct replies + interactionScore += directReplies.length; + + // Keep last few messages for context + const uniqueInteractions = [...new Set(interactions)]; + results.push({ + entity, + interactions: uniqueInteractions.slice(-5), // Only keep last 5 messages for context + count: Math.round(interactionScore) + }); + } + + // Sort by interaction score descending + return results.sort((a, b) => b.count - a.count); +} + +export async function findEntityByName( + runtime: IAgentRuntime, + message: Memory, + state: State, +): Promise { + try { + const room = await runtime.getRoom(message.roomId); + if (!room) { + logger.warn("Room not found for entity search"); + return null; + } + + const world = room.worldId ? await runtime.getWorld(room.worldId) : null; + + // Get all entities in the room with their components + const entitiesInRoom = await runtime.databaseAdapter.getEntitiesForRoom(room.id, runtime.agentId, true); + + // Filter components for each entity based on permissions + const filteredEntities = await Promise.all(entitiesInRoom.map(async entity => { + if (!entity.components) return entity; + + // Get world roles if we have a world + const worldRoles = world?.metadata?.roles || {}; + + // Filter components based on permissions + entity.components = entity.components.filter(component => { + // 1. Pass if sourceEntityId matches the requesting entity + if (component.sourceEntityId === message.userId) return true; + + // 2. Pass if sourceEntityId is an owner/admin of the current world + if (world && component.sourceEntityId) { + const sourceRole = worldRoles[component.sourceEntityId]; + if (sourceRole === "OWNER" || sourceRole === "ADMIN") return true; + } + + // 3. Pass if sourceEntityId is the agentId + if (component.sourceEntityId === runtime.agentId) return true; + + // Filter out components that don't meet any criteria + return false; + }); + + return entity; + })); + + // Get relationships for the message sender + const relationships = await runtime.databaseAdapter.getRelationships({ + userId: message.userId, + agentId: runtime.agentId + }); + + // Get entities from relationships + const relationshipEntities = await Promise.all( + relationships.map(async rel => { + const entityId = rel.sourceEntityId === message.userId ? rel.targetEntityId : rel.sourceEntityId; + return runtime.databaseAdapter.getEntityById(entityId, runtime.agentId); + }) + ); + + // Filter out nulls and combine with room entities + const allEntities = [...filteredEntities, ...relationshipEntities.filter((e): e is Entity => e !== null)]; + + // Get interaction strength data for relationship entities + const interactionData = await getRecentInteractions(runtime, message.userId, allEntities, room.id, relationships); + + // Compose context for LLM + const context = composeContext({ + state: { + ...state, + roomName: room.name || room.id, + worldName: world?.name || "Unknown", + entitiesInRoom: JSON.stringify(filteredEntities, null, 2), + userId: message.userId, + senderId: message.userId, + }, + template: entityResolutionTemplate + }); + + console.log("*** findEntityByName context", context) + + // Use LLM to analyze and resolve the entity + const result = await runtime.useModel(ModelClass.TEXT_LARGE, { + context, + stopSequences: [] + }); + + console.log("*** findEntityByName result", result) + + // Parse LLM response + const resolution = parseJSONObjectFromText(result); + if (!resolution) { + logger.warn("Failed to parse entity resolution result"); + return null; + } + + // If we got an exact entity ID match + if (resolution.type === "EXACT_MATCH" && resolution.entityId) { + const entity = await runtime.databaseAdapter.getEntityById(resolution.entityId as UUID, runtime.agentId); + if (entity) { + // Filter components again for the returned entity + if (entity.components) { + const worldRoles = world?.metadata?.roles || {}; + entity.components = entity.components.filter(component => { + if (component.sourceEntityId === message.userId) return true; + if (world && component.sourceEntityId) { + const sourceRole = worldRoles[component.sourceEntityId]; + if (sourceRole === "OWNER" || sourceRole === "ADMIN") return true; + } + if (component.sourceEntityId === runtime.agentId) return true; + return false; + }); + } + return entity; + } + } + + // For username/name/relationship matches, search through all entities + if (resolution.matches?.[0]?.name) { + const matchName = resolution.matches[0].name.toLowerCase(); + + // Find matching entity by username/handle in components or by name + const matchingEntity = allEntities.find(entity => { + // Check names + if (entity.names.some(n => n.toLowerCase() === matchName)) return true; + + // Check components for username/handle match + return entity.components?.some(c => + c.data.username?.toLowerCase() === matchName || + c.data.handle?.toLowerCase() === matchName + ); + }); + + if (matchingEntity) { + // If this is a relationship match, sort by interaction strength + if (resolution.type === "RELATIONSHIP_MATCH") { + const interactionInfo = interactionData.find(d => d.entity.id === matchingEntity.id); + if (interactionInfo && interactionInfo.count > 0) { + return matchingEntity; + } + } else { + return matchingEntity; + } + } + } + + return null; + } catch (error) { + logger.error("Error in findEntityByName:", error); + return null; + } +} + +export const createUniqueUuid = (runtime, baseUserId: UUID | string): UUID => { + // If the base user ID is the agent ID, return it directly + if (baseUserId === runtime.agentId) { + return runtime.agentId; + } + + // Use a deterministic approach to generate a new UUID based on both IDs + // This creates a unique ID for each user+agent combination while still being deterministic + const combinedString = `${baseUserId}:${runtime.agentId}`; + + // Create a namespace UUID (version 5) from the combined string + return stringToUuid(combinedString); +} \ No newline at end of file diff --git a/packages/core/src/evaluators/fact.ts b/packages/core/src/evaluators/fact.ts deleted file mode 100644 index 9f7991978ee..00000000000 --- a/packages/core/src/evaluators/fact.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { generateObjectArray } from "../generation"; -import { composeContext } from "../context"; -import { MemoryManager } from "../memory"; -import { ActionExample, Evaluator, IAgentRuntime, Memory, ModelClass } from "../types"; - -import { z } from "zod"; - -export const formatFacts = (facts: Memory[]) => { - const messageStrings = facts - .reverse() - .map((fact: Memory) => fact.content.text); - const finalMessageStrings = messageStrings.join("\n"); - return finalMessageStrings; -}; - -const factsTemplate = - // {{actors}} - `# Task: Extract Claims from the conversation - -# START OF EXAMPLES -These are examples of the expected output of this task: -{{evaluationExamples}} -# END OF EXAMPLES - -# INSTRUCTIONS - -Extract any claims from the conversation that are not already present in the list of known facts above: -- Try not to include already-known facts. If you think a fact is already known, but you're not sure, respond with already_known: true. -- If the fact is already in the user's description, set in_bio to true -- If we've already extracted this fact, set already_known to true -- Set the claim type to 'status', 'fact' or 'opinion' -- For true facts about the world or the character that do not change, set the claim type to 'fact' -- For facts that are true but change over time, set the claim type to 'status' -- For non-facts, set the type to 'opinion' -- 'opinion' includes non-factual opinions and also includes the character's thoughts, feelings, judgments or recommendations -- Include any factual detail, including where the user lives, works, or goes to school, what they do for a living, their hobbies, and any other relevant information - -Recent Messages: -{{recentMessages}} - -Response should be a JSON object array inside a JSON markdown block. Correct response format: -\`\`\`json -[ - {"claim": string, "type": enum, in_bio: boolean, already_known: boolean }, - {"claim": string, "type": enum, in_bio: boolean, already_known: boolean }, - ... -] -\`\`\``; - -// Updated schema with an explicit type cast to bypass mismatches between Zod versions. -const claimSchema = (z.array( - z.object({ - claim: z.string(), - type: z.enum(["fact", "opinion", "status"]), - in_bio: z.boolean(), - already_known: z.boolean(), - }) -) as unknown) as any; - -async function handler(runtime: IAgentRuntime, message: Memory) { - const state = await runtime.composeState(message); - - const { agentId, roomId } = state; - - const context = composeContext({ - state, - template: runtime.character.templates?.factsTemplate || factsTemplate, - }); - - const facts = await generateObjectArray({ - runtime, - context, - modelClass: ModelClass.TEXT_LARGE, - schema: claimSchema, - schemaName: "Fact", - schemaDescription: "A fact about the user or the world", - }); - - const factsManager = new MemoryManager({ - runtime, - tableName: "facts", - }); - - if (!facts) { - return []; - } - - // If the fact is known or corrupted, remove it - const filteredFacts = facts - .filter((fact) => { - return ( - !fact.already_known && - fact.type === "fact" && - !fact.in_bio && - fact.claim && - fact.claim.trim() !== "" - ); - }) - .map((fact) => fact.claim); - - for (const fact of filteredFacts) { - const factMemory = await factsManager.addEmbeddingToMemory({ - userId: agentId, - agentId, - content: { text: fact }, - roomId, - createdAt: Date.now(), - }); - - await factsManager.createMemory(factMemory, true); - - await new Promise((resolve) => setTimeout(resolve, 250)); - } - return filteredFacts; -} - -export const factEvaluator: Evaluator = { - name: "GET_FACTS", - similes: [ - "GET_CLAIMS", - "EXTRACT_CLAIMS", - "EXTRACT_FACTS", - "EXTRACT_CLAIM", - "EXTRACT_INFORMATION", - ], - validate: async ( - runtime: IAgentRuntime, - - message: Memory - ): Promise => { - const messageCount = (await runtime.messageManager.countMemories( - message.roomId - )) as number; - - const reflectionCount = Math.ceil(runtime.getConversationLength() / 2); - - return messageCount % reflectionCount === 0; - }, - description: - "Extract factual information about the people in the conversation, the current events in the world, and anything else that might be important to remember.", - handler, - examples: [ - { - context: `Actors in the scene: -{{user1}}: Programmer and moderator of the local story club. -{{user2}}: New member of the club. Likes to write and read. - -Facts about the actors: -None`, - messages: [ - { - user: "{{user1}}", - content: { text: "So where are you from" }, - }, - { - user: "{{user2}}", - content: { text: "I'm from the city" }, - }, - { - user: "{{user1}}", - content: { text: "Which city?" }, - }, - { - user: "{{user2}}", - content: { text: "Oakland" }, - }, - { - user: "{{user1}}", - content: { - text: "Oh, I've never been there, but I know it's in California", - }, - }, - ] as ActionExample[], - outcome: `{ "claim": "{{user2}} is from Oakland", "type": "fact", "in_bio": false, "already_known": false },`, - }, - { - context: `Actors in the scene: -{{user1}}: Athelete and cyclist. Worked out every day for a year to prepare for a marathon. -{{user2}}: Likes to go to the beach and shop. - -Facts about the actors: -{{user1}} and {{user2}} are talking about the marathon -{{user1}} and {{user2}} have just started dating`, - messages: [ - { - user: "{{user1}}", - content: { - text: "I finally completed the marathon this year!", - }, - }, - { - user: "{{user2}}", - content: { text: "Wow! How long did it take?" }, - }, - { - user: "{{user1}}", - content: { text: "A little over three hours." }, - }, - { - user: "{{user1}}", - content: { text: "I'm so proud of myself." }, - }, - ] as ActionExample[], - outcome: `Claims: -json\`\`\` -[ - { "claim": "Alex just completed a marathon in just under 4 hours.", "type": "fact", "in_bio": false, "already_known": false }, - { "claim": "Alex worked out 2 hours a day at the gym for a year.", "type": "fact", "in_bio": true, "already_known": false }, - { "claim": "Alex is really proud of himself.", "type": "opinion", "in_bio": false, "already_known": false } -] -\`\`\` -`, - }, - { - context: `Actors in the scene: -{{user1}}: Likes to play poker and go to the park. Friends with Eva. -{{user2}}: Also likes to play poker. Likes to write and read. - -Facts about the actors: -Mike and Eva won a regional poker tournament about six months ago -Mike is married to Alex -Eva studied Philosophy before switching to Computer Science`, - messages: [ - { - user: "{{user1}}", - content: { - text: "Remember when we won the regional poker tournament last spring", - }, - }, - { - user: "{{user2}}", - content: { - text: "That was one of the best days of my life", - }, - }, - { - user: "{{user1}}", - content: { - text: "It really put our poker club on the map", - }, - }, - ] as ActionExample[], - outcome: `Claims: -json\`\`\` -[ - { "claim": "Mike and Eva won the regional poker tournament last spring", "type": "fact", "in_bio": false, "already_known": true }, - { "claim": "Winning the regional poker tournament put the poker club on the map", "type": "opinion", "in_bio": false, "already_known": false } -] -\`\`\``, - }, - ], -}; diff --git a/packages/core/src/evaluators/goal.ts b/packages/core/src/evaluators/goal.ts index 6b16b7da38c..b695ed211db 100644 --- a/packages/core/src/evaluators/goal.ts +++ b/packages/core/src/evaluators/goal.ts @@ -2,7 +2,7 @@ import { composeContext } from "../context"; import { generateText } from "../generation"; import { getGoals } from "../goals"; import { parseJsonArrayFromText } from "../parsing"; -import { Evaluator, Goal, IAgentRuntime, Memory, ModelClass, State } from "../types"; +import { type Evaluator, type Goal, type IAgentRuntime, type Memory, ModelClass, type State } from "../types"; const goalsTemplate = `# TASK: Update Goal diff --git a/packages/core/src/evaluators/index.ts b/packages/core/src/evaluators/index.ts index 8496906e433..7f7f247673b 100644 --- a/packages/core/src/evaluators/index.ts +++ b/packages/core/src/evaluators/index.ts @@ -1,2 +1,2 @@ -export * from "./fact.ts"; +export * from "./reflection.ts"; export * from "./goal.ts"; diff --git a/packages/core/src/evaluators/reflection.ts b/packages/core/src/evaluators/reflection.ts new file mode 100644 index 00000000000..3b695b46f03 --- /dev/null +++ b/packages/core/src/evaluators/reflection.ts @@ -0,0 +1,292 @@ +import { z } from "zod"; +import { composeContext } from "../context"; +import { generateObject } from "../generation"; +import { MemoryManager } from "../memory"; +import { type Evaluator, type IAgentRuntime, type Memory, ModelClass, type UUID } from "../types"; +import { getActorDetails, resolveActorId } from "../messages"; + +// Schema definitions for the reflection output +const relationshipSchema = z.object({ + sourceEntityId: z.string(), + targetEntityId: z.string(), + tags: z.array(z.string()), + metadata: z.object({ + interactions: z.number(), + }).optional(), +}); + +const reflectionSchema = z.object({ + // reflection: z.string(), + facts: z.array(z.object({ + claim: z.string(), + type: z.string(), + in_bio: z.boolean(), + already_known: z.boolean(), + })), + relationships: z.array(relationshipSchema), +}); + +const reflectionTemplate = `# Task: Generate Agent Reflection, Extract Facts and Relationships + +# Examples: +{{evaluationExamples}} + +{{actors}} + +{{bio}} + +# Entities in Room +{{entitiesInRoom}} + +# Existing Relationships +{{existingRelationships}} + +# Current Context: +Agent Name: {{agentName}} +Room Type: {{roomType}} +Message Sender: {{senderName}} (ID: {{senderId}}) + +{{recentMessages}} + +# Known Facts: +{{knownFacts}} + +# Instructions: +1. Extract new facts from the conversation +2. Identify and describe relationships between entities. The sourceEntityId is the UUID of the entity initiating the interaction. The targetEntityId is the UUID of the entity being interacted with. Relationships are one-direction, so a friendship would be two entity relationships where each entity is both the source and the target of the other. + +Generate a response in the following format: +\`\`\`json +{ + "facts": [ + { + "claim": "factual statement", + "type": "fact|opinion|status", + "in_bio": false, + "already_known": false + } + ], + "relationships": [ + { + "sourceEntityId": "entity_initiating_interaction", + "targetEntityId": "entity_being_interacted_with", + "tags": ["group_interaction|voice_interaction|dm_interaction", "additional_tag1", "additional_tag2"] + } + ] +} +\`\`\``; + +async function handler(runtime: IAgentRuntime, message: Memory) { + const state = await runtime.composeState(message); + const { agentId, roomId } = state; + + // Get existing relationships for the room + const existingRelationships = await runtime.databaseAdapter.getRelationships({ + userId: message.userId, + agentId + }); + + // Get actors in the room for name resolution + const actors = await getActorDetails({ runtime, roomId }); + + const entitiesInRoom = await runtime.databaseAdapter.getEntitiesForRoom(roomId, agentId); + + // Get known facts + const factsManager = new MemoryManager({ + runtime, + tableName: "facts", + }); + + const knownFacts = await factsManager.getMemories({ + roomId, + agentId, + count: 30, + unique: true, + }); + + const context = composeContext({ + state: { + ...state, + knownFacts: formatFacts(knownFacts), + roomType: state.roomType || "group", // Can be "group", "voice", or "dm" + entitiesInRoom: JSON.stringify(entitiesInRoom), + existingRelationships: JSON.stringify(existingRelationships), + senderId: message.userId + }, + template: runtime.character.templates?.reflectionTemplate || reflectionTemplate, + }); + + const reflection = await generateObject({ + runtime, + context, + modelClass: ModelClass.TEXT_LARGE, + schema: reflectionSchema, + schemaName: "Reflection", + schemaDescription: "Agent reflection including facts and relationships", + }); + + // Store new facts + const newFacts = reflection.facts.filter(fact => + !fact.already_known && + !fact.in_bio && + fact.claim && + fact.claim.trim() !== "" + ); + + for (const fact of newFacts) { + const factMemory = await factsManager.addEmbeddingToMemory({ + userId: agentId, + agentId, + content: { text: fact.claim }, + roomId, + createdAt: Date.now(), + }); + await factsManager.createMemory(factMemory, true); + } + + // Update or create relationships + for (const relationship of reflection.relationships) { + let sourceId: UUID; + let targetId: UUID; + + try { + sourceId = resolveActorId(relationship.sourceEntityId, actors); + targetId = resolveActorId(relationship.targetEntityId, actors); + } catch (error) { + console.warn('Failed to resolve relationship entities:', error); + console.warn('relationship:\n', relationship) + continue; // Skip this relationship if we can't resolve the IDs + } + + const existingRelationship = existingRelationships.find(r => + r.sourceEntityId === sourceId && + r.targetEntityId === targetId + ); + + if (existingRelationship) { + const updatedMetadata = { + ...existingRelationship.metadata, + interactions: (existingRelationship.metadata?.interactions || 0) + 1 + }; + + const updatedTags = Array.from(new Set([ + ...(existingRelationship.tags || []), + ...relationship.tags + ])); + + await runtime.databaseAdapter.updateRelationship({ + id: existingRelationship.id, + sourceEntityId: sourceId, + targetEntityId: targetId, + agentId, + tags: updatedTags, + metadata: updatedMetadata, + }); + } else { + await runtime.databaseAdapter.createRelationship({ + sourceEntityId: sourceId, + targetEntityId: targetId, + agentId, + tags: relationship.tags, + metadata: { + interactions: 1, + ...relationship.metadata + } + }); + } + } + + await runtime.cacheManager.set(`${message.roomId}-reflection-last-processed`, message.id); + + return reflection; +} + +export const reflectionEvaluator: Evaluator = { + name: "REFLECTION", + similes: [ + "REFLECT", + "SELF_REFLECT", + "EVALUATE_INTERACTION", + "ASSESS_SITUATION" + ], + validate: async ( + runtime: IAgentRuntime, + message: Memory + ): Promise => { + const lastMessageId = await runtime.cacheManager.get(`${message.roomId}-reflection-last-processed`) + const messages = await runtime.messageManager.getMemories({ roomId: message.roomId, count: runtime.getConversationLength() }) + + if (lastMessageId) { + const lastMessageIndex = messages.findIndex(msg => msg.id === lastMessageId); + if (lastMessageIndex !== -1) { + messages.splice(0, lastMessageIndex + 1); + } + } + + const reflectionInterval = Math.ceil(runtime.getConversationLength() / 4); + + return messages.length > reflectionInterval; + }, + description: "Generate self-reflection, extract facts, and track relationships between entities in the conversation.", + handler, + examples: [ + { + context: `Agent Name: Sarah +Agent Role: Community Manager +Room Type: group +Current Room: general-chat +Message Sender: John (user-123)`, + messages: [ + { + user: "John", + content: { text: "Hey everyone, I'm new here!" }, + }, + { + user: "Sarah", + content: { text: "Welcome John! How did you find our community?" }, + }, + { + user: "John", + content: { text: "Through a friend who's really into AI" }, + } + ], + outcome: `{ + "reflection": "I'm engaging appropriately with a new community member, maintaining a welcoming and professional tone. My questions are helping to learn more about John and make him feel welcome.", + "facts": [ + { + "claim": "John is new to the community", + "type": "fact", + "in_bio": false, + "already_known": false + }, + { + "claim": "John found the community through a friend interested in AI", + "type": "fact", + "in_bio": false, + "already_known": false + } + ], + "relationships": [ + { + "sourceEntityId": "sarah-agent", + "targetEntityId": "user-123", + "tags": ["group_interaction"] + }, + { + "sourceEntityId": "user-123", + "targetEntityId": "sarah-agent", + "tags": ["group_interaction"] + } + ] +}` + } + ] +}; + +// Helper function to format facts for context +function formatFacts(facts: Memory[]) { + return facts + .reverse() + .map((fact: Memory) => fact.content.text) + .join("\n"); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index aba3d565156..54f41be6b90 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,6 +7,7 @@ export * from "./cache.ts"; export * from "./context.ts"; export * from "./database.ts"; export * from "./environment.ts"; +export * from "./entities.ts"; export * from "./evaluators.ts"; export * from "./generation.ts"; export * from "./goals.ts"; @@ -18,8 +19,8 @@ export * from "./messages.ts"; export * from "./parsing.ts"; export * from "./posts.ts"; export * from "./providers.ts"; -export * from "./relationships.ts"; export * from "./roles.ts"; export * from "./runtime.ts"; export * from "./settings.ts"; +export * from "./bootstrap.ts"; export * from "./uuid.ts"; diff --git a/packages/core/src/knowledge.ts b/packages/core/src/knowledge.ts index 3a09f875d97..277827cf748 100644 --- a/packages/core/src/knowledge.ts +++ b/packages/core/src/knowledge.ts @@ -1,8 +1,8 @@ -import { splitChunks } from "./parsing.ts"; +import { createUniqueUuid } from "./entities.ts"; import logger from "./logger.ts"; +import { splitChunks } from "./parsing.ts"; import type { AgentRuntime } from "./runtime.ts"; -import { type KnowledgeItem, type Memory, ModelClass, type UUID, MemoryType } from "./types.ts"; -import { stringToUuid } from "./uuid.ts"; +import { type KnowledgeItem, type Memory, MemoryType, ModelClass, type UUID } from "./types.ts"; async function get( runtime: AgentRuntime, @@ -105,7 +105,7 @@ async function set( // Store each fragment with link to source document for (let i = 0; i < fragments.length; i++) { const fragmentMemory: Memory = { - id: stringToUuid(`${item.id}-fragment-${i}`), + id: createUniqueUuid(this.runtime, `${item.id}-fragment-${i}`), agentId: runtime.agentId, roomId: runtime.agentId, userId: runtime.agentId, diff --git a/packages/core/src/memory.ts b/packages/core/src/memory.ts index db75ce9b174..8e1b507c6f9 100644 --- a/packages/core/src/memory.ts +++ b/packages/core/src/memory.ts @@ -70,7 +70,7 @@ export class MemoryManager implements IMemoryManager { private transformUserIdIfNeeded(memory: Memory): Memory { return { ...memory, - userId: this.runtime.generateTenantUserId(memory.userId) + userId: memory.userId }; } @@ -204,7 +204,7 @@ export class MemoryManager implements IMemoryManager { * @param unique Whether to check for similarity before insertion. * @returns A Promise that resolves when the operation completes. */ - async createMemory(memory: Memory, unique = false): Promise { + async createMemory(memory: Memory, unique = false): Promise { memory = this.transformUserIdIfNeeded(memory); if (memory.metadata) { @@ -248,11 +248,13 @@ export class MemoryManager implements IMemoryManager { memory.embedding = await this.runtime.useModel(ModelClass.TEXT_EMBEDDING, null); } - await this.runtime.databaseAdapter.createMemory( + const memoryId = await this.runtime.databaseAdapter.createMemory( memory, this.tableName, unique ); + + return memoryId; } async getMemoriesByRoomIds(params: { roomIds: UUID[], limit?: number; agentId?: UUID }): Promise { diff --git a/packages/core/src/messages.ts b/packages/core/src/messages.ts index 3a85825c8d4..b676bcc8ec0 100644 --- a/packages/core/src/messages.ts +++ b/packages/core/src/messages.ts @@ -1,10 +1,4 @@ -import { v4 } from "uuid"; -import { composeContext } from "./context.ts"; -import { generateMessageResponse, generateShouldRespond } from "./generation.ts"; -import { logger } from "./logger.ts"; -import { messageCompletionFooter, shouldRespondFooter } from "./parsing.ts"; -import { ModelClass, type Actor, type Content, type HandlerCallback, type IAgentRuntime, type Memory, type State, type UUID } from "./types.ts"; -import { stringToUuid } from "./uuid.ts"; +import type { Actor, Content, IAgentRuntime, Memory, UUID } from "./types.ts"; export * as actions from "./actions"; export * as evaluators from "./evaluators"; export * as providers from "./providers"; @@ -19,25 +13,33 @@ export async function getActorDetails({ runtime: IAgentRuntime; roomId: UUID; }) { - const participantIds = await runtime.databaseAdapter.getParticipantsForRoom( - roomId, - runtime.agentId - ); - - // Fetch all actor details - const actors = await Promise.all( - participantIds.map(async (userId) => { - const account = await runtime.databaseAdapter.getEntityById(userId, runtime.agentId); - if (account) { - return { - id: account.id, - name: account.metadata.name, - username: account.metadata.username, - }; + const room = await runtime.getRoom(roomId); + const entities = await runtime.databaseAdapter.getEntitiesForRoom(roomId, runtime.agentId, true); + const actors = entities.map(entity => { + // join all fields of all component.data together + const allData = entity.components.reduce((acc, component) => { + return { ...acc, ...component.data }; + }, {}); + + // combine arrays and merge the values of objects + const mergedData = Object.entries(allData).reduce((acc, [key, value]) => { + if (!acc[key]) { + acc[key] = value; + } else if (Array.isArray(acc[key]) && Array.isArray(value)) { + acc[key] = [...new Set([...acc[key], ...value])]; + } else if (typeof acc[key] === 'object' && typeof value === 'object') { + acc[key] = { ...acc[key], ...value }; } - return null; - }) - ); + return acc; + }, {}); + + return { + id: entity.id, + name: entity.metadata[room.source]?.name || entity.names[0], + names: entity.names, + data: JSON.stringify({...mergedData, ...entity.metadata}) + }; + }); // Filter out nulls and ensure uniqueness by ID const uniqueActors = new Map(); @@ -59,13 +61,36 @@ export async function getActorDetails({ */ export function formatActors({ actors }: { actors: Actor[] }) { const actorStrings = actors.map((actor: Actor) => { - const header = `${actor.name}`; + const header = `${actor.name} (${actor.names.join(" aka ")})\nID: ${actor.id}${(actor.data && Object.keys(actor.data).length > 0) ? `\nData: ${JSON.stringify(actor.data)}\n` : "\n"}`; return header; }); const finalActorStrings = actorStrings.join("\n"); return finalActorStrings; } +/** + * Resolve an actor name to their UUID + * @param name - Name to resolve + * @param actors - List of actors to search through + * @returns UUID if found, throws error if not found or if input is not a valid UUID + */ +export function resolveActorId(name: string, actors: Actor[]): UUID { + // If the name is already a valid UUID, return it + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(name)) { + return name as UUID; + } + + const actor = actors.find(a => + a.names.some(n => n.toLowerCase() === name.toLowerCase()) + ); + + if (!actor) { + throw new Error(`Could not resolve name "${name}" to a valid UUID`); + } + + return actor.id; +} + /** * Format messages into a string * @param {Object} params - The formatting parameters @@ -99,12 +124,21 @@ export const formatMessages = ({ .join(", ")})` : ""; + const messageTime = new Date(message.createdAt); + const hours = messageTime.getHours().toString().padStart(2, '0'); + const minutes = messageTime.getMinutes().toString().padStart(2, '0'); + const timeString = `${hours}:${minutes}`; + const timestamp = formatTimestamp(message.createdAt); const shortId = message.userId.slice(-5); - return `(${timestamp}) [${shortId}] ${formattedName}: ${messageContent}${attachmentString}${ - messageAction && messageAction !== "null" ? ` (${messageAction})` : "" + if(messageAction === "REFLECTION") { + return `${timeString} (${timestamp}) [${shortId}] ${formattedName} (internal monologue) *${messageContent}*`; + } + + return `${timeString} (${timestamp}) [${shortId}] ${formattedName}: ${messageContent}${attachmentString}${ + messageAction && messageAction !== "null" ? ` (action: ${messageAction})` : "" }`; }) .join("\n"); diff --git a/packages/core/src/parsing.ts b/packages/core/src/parsing.ts index 17561e29b9e..4c1cb1ab484 100644 --- a/packages/core/src/parsing.ts +++ b/packages/core/src/parsing.ts @@ -5,10 +5,10 @@ const jsonBlockPattern = /```json\n([\s\S]*?)\n```/; export const messageCompletionFooter = `\nResponse format should be formatted in a valid JSON block like this: \`\`\`json -{ "user": "{{agentName}}", "text": "", "action": "" } +{ "thought": "", "user": "{{agentName}}", "text": "", "action": "" } \`\`\` -The "action" field should be one of the options in [Available Actions] and the "text" field should be the response you want to send. +The "action" field should be one of the options in [Available Actions] and the "text" field should be the response you want to send. Do not including any thinking or internal reflection in the "text" field. "thought" should be a short description of what the agent is thinking about before responding, inlcuding a brief justification for the response. `; export const shouldRespondFooter = "The available options are RESPOND, IGNORE, or STOP. Choose the most appropriate option."; @@ -336,7 +336,7 @@ export function truncateToCompleteSentence( // Assuming ~4 tokens per character on average const TOKENS_PER_CHAR = 4; const TARGET_TOKENS = 3000; -const TARGET_CHARS = Math.floor(TARGET_TOKENS / TOKENS_PER_CHAR); // ~750 chars +const _TARGET_CHARS = Math.floor(TARGET_TOKENS / TOKENS_PER_CHAR); // ~750 chars export async function splitChunks( content: string, diff --git a/packages/core/src/posts.ts b/packages/core/src/posts.ts index 71f098cd7c0..3cf3222732b 100644 --- a/packages/core/src/posts.ts +++ b/packages/core/src/posts.ts @@ -40,8 +40,9 @@ export const formatPosts = ({ const actor = actors.find( (actor: Actor) => actor.id === message.userId ); + // TODO: These are okay but not great const userName = actor?.name || "Unknown User"; - const displayName = actor?.username || "unknown"; + const displayName = actor?.names[0] || "unknown"; return `Name: ${userName} (@${displayName}) ID: ${message.id}${message.content.inReplyTo ? `\nIn reply to: ${message.content.inReplyTo}` : ""} diff --git a/packages/core/src/providers/confirmation.ts b/packages/core/src/providers/confirmation.ts deleted file mode 100644 index 450181d4971..00000000000 --- a/packages/core/src/providers/confirmation.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { logger } from "../logger"; -import { IAgentRuntime, Memory, Provider, State } from "../types"; - -export const confirmationTasksProvider: Provider = { - get: async ( - runtime: IAgentRuntime, - message: Memory, - _state?: State - ): Promise => { - try { - // Get all pending tasks for this room - const pendingTasks = runtime.getTasks({ - roomId: message.roomId, - tags: ["AWAITING_CONFIRMATION"] - }); - - if (!pendingTasks || pendingTasks.length === 0) { - return ""; - } - - // Format tasks into a readable list - let output = "# Pending Tasks\n\n"; - output += "The following tasks are awaiting confirmation:\n\n"; - - pendingTasks.forEach((task, index) => { - output += `${index + 1}. **${task.name}**\n`; - if (task.description) { - output += ` ${task.description}\n`; - } - output += "\n"; - }); - - output += "To confirm a task, say 'confirm' or 'approve'.\n"; - output += "To cancel a task, say 'cancel' or 'reject'.\n"; - - return output.trim(); - } catch (error) { - logger.error("Error in confirmation tasks provider:", error); - return "Error retrieving pending tasks."; - } - } -}; - -export default confirmationTasksProvider; \ No newline at end of file diff --git a/packages/core/src/providers/facts.ts b/packages/core/src/providers/facts.ts index 4ee08c6d79c..19e8b189214 100644 --- a/packages/core/src/providers/facts.ts +++ b/packages/core/src/providers/facts.ts @@ -1,8 +1,14 @@ -import { formatFacts } from "../evaluators/fact.ts"; import { MemoryManager } from "../memory.ts"; import { formatMessages } from "../messages.ts"; -import { IAgentRuntime, Memory, ModelClass, Provider, State } from "../types.ts"; +import { type IAgentRuntime, type Memory, ModelClass, type Provider, type State } from "../types.ts"; + +function formatFacts(facts: Memory[]) { + return facts + .reverse() + .map((fact: Memory) => fact.content.text) + .join("\n"); +} const factsProvider: Provider = { get: async (runtime: IAgentRuntime, message: Memory, state?: State) => { diff --git a/packages/core/src/providers/options.ts b/packages/core/src/providers/options.ts new file mode 100644 index 00000000000..fc267391aa4 --- /dev/null +++ b/packages/core/src/providers/options.ts @@ -0,0 +1,75 @@ +import { logger } from "../logger"; +import type { IAgentRuntime, Memory, Provider, State } from "../types"; + +// Define an interface for option objects +interface OptionObject { + name: string; + description?: string; +} + +export const optionsProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + message: Memory, + _state?: State + ): Promise => { + try { + // Get all pending tasks for this room with options + const pendingTasks = runtime.getTasks({ + roomId: message.roomId, + tags: ["AWAITING_CHOICE"] + }); + + if (!pendingTasks || pendingTasks.length === 0) { + return ""; + } + + // Filter tasks that have options + const tasksWithOptions = pendingTasks.filter(task => task.metadata?.options); + + if (tasksWithOptions.length === 0) { + return ""; + } + + // Format tasks into a readable list + let output = "# Pending Tasks\n\n"; + output += "The following tasks are awaiting your selection:\n\n"; + + tasksWithOptions.forEach((task, index) => { + output += `${index + 1}. **${task.name}**\n`; + if (task.description) { + output += ` ${task.description}\n`; + } + + // List available options + if (task.metadata?.options) { + output += " Options:\n"; + + // Handle both string[] and OptionObject[] formats + const options = task.metadata.options as string[] | OptionObject[]; + + options.forEach(option => { + if (typeof option === 'string') { + // Handle string option + const description = task.metadata?.options.find(o => o.name === option)?.description || ''; + output += ` - \`${option}\` ${description ? `- ${description}` : ''}\n`; + } else { + // Handle option object + output += ` - \`${option.name}\` ${option.description ? `- ${option.description}` : ''}\n`; + } + }); + } + output += "\n"; + }); + + output += "To select an option, reply with the option name (e.g., 'post' or 'cancel').\n"; + + return output.trim(); + } catch (error) { + logger.error("Error in options provider:", error); + return "Error retrieving pending tasks with options."; + } + } +}; + +export default optionsProvider; \ No newline at end of file diff --git a/packages/core/src/providers/relationships.ts b/packages/core/src/providers/relationships.ts new file mode 100644 index 00000000000..278b4b5f8bd --- /dev/null +++ b/packages/core/src/providers/relationships.ts @@ -0,0 +1,75 @@ +import type { IAgentRuntime, Memory, Provider, State, Relationship, UUID } from "../types.ts"; + +async function getRelationships({ + runtime, + userId, +}: { + runtime: IAgentRuntime; + userId: UUID; +}) { + return runtime.databaseAdapter.getRelationships({ userId, agentId: runtime.agentId }); +} + +async function formatRelationships(runtime: IAgentRuntime, relationships: Relationship[]) { + // Sort relationships by interaction strength (descending) + const sortedRelationships = relationships + .filter(rel => rel.metadata?.interactions) + .sort((a, b) => + (b.metadata?.interactions || 0) - (a.metadata?.interactions || 0) + ) + .slice(0, 30); // Get top 30 + + if (sortedRelationships.length === 0) { + return ""; + } + + const formattedRelationships = await Promise.all(sortedRelationships + .map(async (rel, _index) => { + + const formatMetadata = (metadata: any) => { + return JSON.stringify(Object.entries(metadata) + .map(([key, value]) => `${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`) + .join("\n")); + } + + // get the targetEntityId + const targetEntityId = rel.targetEntityId; + + // get the entity + const entity = await runtime.getEntity(targetEntityId); + + if(!entity) { + return null; + } + + const names = entity.names.join(" aka "); + return `${names}\n${rel.tags ? rel.tags.join(", ") : ""}\n${formatMetadata(entity.metadata)}\n`; + }) + ); + + return formattedRelationships.filter(Boolean).join("\n"); +} + +const relationshipsProvider: Provider = { + get: async (runtime: IAgentRuntime, message: Memory, state?: State) => { + // Get all relationships for the current user + const relationships = await getRelationships({ + runtime, + userId: message.userId, + }); + + if (!relationships || relationships.length === 0) { + return ""; + } + + const formattedRelationships = await formatRelationships(runtime, relationships); + + if (!formattedRelationships) { + return ""; + } + + return `${runtime.character.name} has observed ${state.senderName} interacting with these people:\n${formattedRelationships}`; + }, +}; + +export { relationshipsProvider }; \ No newline at end of file diff --git a/packages/core/src/providers/roles.ts b/packages/core/src/providers/roles.ts index 76bccd658ce..c3c240d90a5 100644 --- a/packages/core/src/providers/roles.ts +++ b/packages/core/src/providers/roles.ts @@ -1,6 +1,6 @@ +import { createUniqueUuid } from "../entities"; import { logger } from "../logger"; -import { Provider, IAgentRuntime, Memory, State, ChannelType, UUID } from "../types"; -import { stringToUuid } from "../uuid"; +import { ChannelType, type IAgentRuntime, type Memory, type Provider, type State, type UUID } from "../types"; export const roleProvider: Provider = { get: async ( @@ -27,7 +27,7 @@ export const roleProvider: Provider = { logger.info(`Using server ID: ${serverId}`); // Get world data instead of using cache - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, serverId); const world = await runtime.getWorld(worldId); if (!world || !world.metadata?.ownership?.ownerId) { @@ -46,9 +46,9 @@ export const roleProvider: Provider = { logger.info(`Found ${Object.keys(roles).length} roles`); // Group users by role - const owners: { name: string, username: string }[] = []; - const admins: { name: string, username: string }[] = []; - const members: { name: string, username: string }[] = []; + const owners: { name: string, username: string, names: string[] }[] = []; + const admins: { name: string, username: string, names: string[] }[] = []; + const members: { name: string, username: string, names: string[] }[] = []; // Process roles for (const userId in roles) { @@ -57,8 +57,9 @@ export const roleProvider: Provider = { // get the user from the database const user = await runtime.getEntity(userId as UUID); - const name = user.metadata[message.content.source ?? room.source]?.name ?? user.metadata.default.name; - const username = user.metadata[message.content.source ?? room.source].username ?? user.metadata.default.username; + const name = user.metadata[room.source]?.name; + const username = user.metadata[room.source]?.username; + const names = user.names; // Skip duplicates (we store both UUID and original ID) if (owners.some(owner => owner.username === username) || admins.some(admin => admin.username === username) || members.some(member => member.username === username)) { @@ -68,13 +69,13 @@ export const roleProvider: Provider = { // Add to appropriate group switch (userRole) { case "OWNER": - owners.push({ name, username }); + owners.push({ name, username, names }); break; case "ADMIN": - admins.push({ name, username }); + admins.push({ name, username, names }); break; default: - members.push({ name, username }); + members.push({ name, username, names }); break; } } @@ -85,7 +86,7 @@ export const roleProvider: Provider = { if (owners.length > 0) { response += "## Owners\n"; owners.forEach(owner => { - response += `${owner.name} (${owner.username})\n`; + response += `${owner.name} (${owner.names.join(", ")})\n`; }); response += "\n"; } @@ -93,7 +94,7 @@ export const roleProvider: Provider = { if (admins.length > 0) { response += "## Administrators\n"; admins.forEach(admin => { - response += `${admin.name} (${admin.username})\n`; + response += `${admin.name} (${admin.names.join(", ")}) (${admin.username})\n`; }); response += "\n"; } @@ -101,7 +102,7 @@ export const roleProvider: Provider = { if (members.length > 0) { response += "## Members\n"; members.forEach(member => { - response += `${member.name} (${member.username})\n`; + response += `${member.name} (${member.names.join(", ")}) (${member.username})\n`; }); } diff --git a/packages/core/src/providers/settings.ts b/packages/core/src/providers/settings.ts index 8e71419aaa9..af45596379c 100644 --- a/packages/core/src/providers/settings.ts +++ b/packages/core/src/providers/settings.ts @@ -6,10 +6,10 @@ import { findWorldForOwner } from "../roles"; import { getWorldSettings } from "../settings"; import { ChannelType, - IAgentRuntime, - Memory, - Provider, - State, + type IAgentRuntime, + type Memory, + type Provider, + type State, type OnboardingSetting, type WorldSettings } from "../types"; @@ -30,7 +30,7 @@ const formatSettingValue = ( * Generates a status message based on the current settings state */ function generateStatusMessage( - runtime: IAgentRuntime, + _runtime: IAgentRuntime, worldSettings: WorldSettings, isOnboarding: boolean, state?: State @@ -38,7 +38,7 @@ function generateStatusMessage( try { // Format settings for display const formattedSettings = Object.entries(worldSettings) - .map(([key, setting]) => { + .map(([_key, setting]) => { if (typeof setting !== "object" || !setting.name) return null; const description = setting.description || ""; @@ -69,32 +69,24 @@ function generateStatusMessage( if (isOnboarding) { if (requiredUnconfigured > 0) { return ( - `# PRIORITY TASK: Onboarding with ${state.senderName}\n${state.agentName} still needs to configure ${requiredUnconfigured} required settings:\n\n` + - formattedSettings + `# PRIORITY TASK: Onboarding with ${state.senderName}\n${state.agentName} still needs to configure ${requiredUnconfigured} required settings:\n\n${formattedSettings .filter((s) => s.required && !s.configured) .map((s) => `${s.name}: ${s.usageDescription}\nValue: ${s.value}`) - .join("\n\n") + - "\n\n" + - `If the user gives any information related to the settings, ${state.agentName} should use the UPDATE_SETTINGS action to update the settings with this new information. ${state.agentName} can update any, some or all settings.` + .join("\n\n")}\n\nIf the user gives any information related to the settings, ${state.agentName} should use the UPDATE_SETTINGS action to update the settings with this new information. ${state.agentName} can update any, some or all settings.` ); - } else { + } return ( - "All required settings have been configured! Here's the current configuration:\n\n" + - formattedSettings.map((s) => `${s.name}: ${s.description}\nValue: ${s.value}`).join("\n") + `All required settings have been configured! Here's the current configuration:\n\n${formattedSettings.map((s) => `${s.name}: ${s.description}\nValue: ${s.value}`).join("\n")}` ); - } - } else { + } // Non-onboarding context - list all public settings with values and descriptions return ( - "## Current Configuration\n\n" + - (requiredUnconfigured > 0 + `## Current Configuration\n\n${requiredUnconfigured > 0 ? `**Note:** ${requiredUnconfigured} required settings still need configuration.\n\n` - : "All required settings are configured.\n\n") + - formattedSettings + : "All required settings are configured.\n\n"}${formattedSettings .map((s) => `### ${s.name}\n**Value:** ${s.value}\n**Description:** ${s.description}`) - .join("\n\n") + .join("\n\n")}` ); - } } catch (error) { logger.error(`Error generating status message: ${error}`); return "Error generating configuration status."; diff --git a/packages/core/src/providers/time.ts b/packages/core/src/providers/time.ts index c5c518990e8..61c5f9c1764 100644 --- a/packages/core/src/providers/time.ts +++ b/packages/core/src/providers/time.ts @@ -1,4 +1,4 @@ -import { IAgentRuntime, Memory, Provider, State } from "../types"; +import type { IAgentRuntime, Memory, Provider, State } from "../types"; const timeProvider: Provider = { get: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => { diff --git a/packages/core/src/relationships.ts b/packages/core/src/relationships.ts deleted file mode 100644 index 82c735a8292..00000000000 --- a/packages/core/src/relationships.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { IAgentRuntime, Relationship, UUID } from "./types.ts"; - -export async function createRelationship({ - runtime, - userA, - userB, -}: { - runtime: IAgentRuntime; - userA: UUID; - userB: UUID; -}): Promise { - return runtime.databaseAdapter.createRelationship({ - userA, - userB, - agentId: runtime.agentId, - }); -} - -export async function getRelationship({ - runtime, - userA, - userB, -}: { - runtime: IAgentRuntime; - userA: UUID; - userB: UUID; -}) { - return runtime.databaseAdapter.getRelationship({ - userA, - userB, - agentId: runtime.agentId, - }); -} - -export async function getRelationships({ - runtime, - userId, -}: { - runtime: IAgentRuntime; - userId: UUID; -}) { - return runtime.databaseAdapter.getRelationships({ userId, agentId: runtime.agentId }); -} - -export async function formatRelationships({ - runtime, - userId, -}: { - runtime: IAgentRuntime; - userId: UUID; -}) { - const relationships = await getRelationships({ runtime, userId }); - - const formattedRelationships = relationships.map( - (relationship: Relationship) => { - const { userA, userB } = relationship; - - if (userA === userId) { - return userB; - } - - return userA; - } - ); - - return formattedRelationships; -} diff --git a/packages/core/src/roles.ts b/packages/core/src/roles.ts index 068ca1e4d9f..b7f85477a7c 100644 --- a/packages/core/src/roles.ts +++ b/packages/core/src/roles.ts @@ -2,8 +2,7 @@ // Updated to use world metadata instead of cache import { logger } from "./logger"; -import { IAgentRuntime, WorldData } from "./types"; -import { stringToUuid } from "./uuid"; +import type { IAgentRuntime, WorldData } from "./types"; export interface ServerOwnershipState { servers: { @@ -11,18 +10,6 @@ export interface ServerOwnershipState { }; } -/** - * Normalizes user IDs to UUID format - * Both stringToUuid and direct values are supported for robustness - */ -export function normalizeUserId(id: string): string { - // Avoid double-conversion by checking if already a UUID format - if (id.includes("-") && id.length === 36) { - return id; - } - return stringToUuid(id); -} - /** * Finds a server where the given user is the owner */ @@ -36,11 +23,6 @@ export async function findWorldForOwner( return null; } - const normalizedUserId = normalizeUserId(userId); - logger.info( - `Looking for server where ${normalizedUserId} is owner (original ID: ${userId})` - ); - // Get all worlds for this agent const worlds = await runtime.getAllWorlds(); @@ -51,9 +33,9 @@ export async function findWorldForOwner( // Find world where the user is the owner for (const world of worlds) { - if (world.metadata?.ownership?.ownerId === normalizedUserId) { + if (world.metadata?.ownership?.ownerId === userId) { logger.info( - `Found server ${world.serverId} for owner ${normalizedUserId}` + `Found server ${world.serverId} for owner ${userId}` ); return world; } @@ -67,7 +49,7 @@ export async function findWorldForOwner( } } - logger.info(`No server found for owner ${normalizedUserId}`); + logger.info(`No server found for owner ${userId}`); return null; } catch (error) { logger.error(`Error finding server for owner: ${error}`); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 5b977f73f01..5f67a631c52 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -16,7 +16,7 @@ import { formatEvaluators, } from "./evaluators.ts"; import { generateText } from "./generation.ts"; -import { handlePluginImporting, logger } from "./index.ts"; +import { createUniqueUuid, handlePluginImporting, logger } from "./index.ts"; import knowledge from "./knowledge.ts"; import { MemoryManager } from "./memory.ts"; import { formatActors, formatMessages, getActorDetails } from "./messages.ts"; @@ -49,7 +49,7 @@ import { type State, type Task, type UUID, - type WorldData, + type WorldData } from "./types.ts"; import { stringToUuid } from "./uuid.ts"; @@ -63,7 +63,7 @@ function formatKnowledge(knowledge: KnowledgeItem[]): string { class KnowledgeManager { private runtime: AgentRuntime; - constructor(runtime: AgentRuntime, knowledgeRoot: string) { + constructor(runtime: AgentRuntime, _knowledgeRoot: string) { this.runtime = runtime; } @@ -85,7 +85,7 @@ class KnowledgeManager { async processCharacterKnowledge(items: string[]) { for (const item of items) { try { - const knowledgeId = stringToUuid(item); + const knowledgeId = createUniqueUuid(this.runtime, item); if (await this.checkExistingKnowledge(knowledgeId)) { continue; } @@ -419,28 +419,24 @@ export class AgentRuntime implements IAgentRuntime { async initialize() { // First create the agent entity directly try { + await this.ensureAgentExists(); + // No need to transform agent's own ID const agentEntity = await this.databaseAdapter.getEntityById( this.agentId, this.agentId ); + if (!agentEntity) { const created = await this.databaseAdapter.createEntity({ id: this.agentId, agentId: this.agentId, names: Array.from( new Set( - [this.character.name, this.character.username].filter(Boolean) + [this.character.name].filter(Boolean) ) ) as string[], - metadata: { - originalUserId: this.agentId, - default: { - name: this.character.name || "Agent", - username: - this.character.username || this.character.name || "Agent", - }, - }, + metadata: {}, }); if (!created) { @@ -460,9 +456,6 @@ export class AgentRuntime implements IAgentRuntime { throw error; } - // Continue with agent setup - await this.ensureAgentExists(); - // Load plugins before trying to access models or services if (this.character.plugins) { const plugins = (await handlePluginImporting( @@ -541,7 +534,7 @@ export class AgentRuntime implements IAgentRuntime { // Create room for the agent try { await this.ensureRoomExists({ - id: this.generateTenantUserId(this.agentId), + id: this.agentId, name: this.character.name, source: "self", type: ChannelType.SELF, @@ -626,20 +619,6 @@ export class AgentRuntime implements IAgentRuntime { ); } - generateTenantUserId(baseUserId: UUID): UUID { - // If the base user ID is the agent ID, return it directly - if (baseUserId === this.agentId) { - return this.agentId; - } - - // Use a deterministic approach to generate a new UUID based on both IDs - // This creates a unique ID for each user+agent combination while still being deterministic - const combinedString = `${baseUserId}:${this.agentId}`; - - // Create a namespace UUID (version 5) from the combined string - return stringToUuid(combinedString); - } - async ensureAgentExists() { const agent = await this.databaseAdapter.getAgent(this.agentId); if (!agent) { @@ -895,97 +874,50 @@ export class AgentRuntime implements IAgentRuntime { */ async getOrCreateUser( userId: UUID, - userName: string | null, - name: string | null, - source: string | null + names: string[], + metadata: { + [source: string]: { + name: string; + userName: string; + }; + } ) { - // Generate tenant-specific user ID - apply the transformation - const tenantSpecificUserId = this.generateTenantUserId(userId); - const account = await this.databaseAdapter.getEntityById( - tenantSpecificUserId, + userId, this.agentId ); if (!account) { const created = await this.databaseAdapter.createEntity({ - id: tenantSpecificUserId, + id: userId, agentId: this.agentId, - names: Array.from( - new Set([name, userName].filter(Boolean)) - ) as string[], - metadata: { - default: { - name: name || "Unknown User", - username: userName || "Unknown", - }, - [source]: { - name: name || "Unknown User", - username: userName || "Unknown", - }, - originalUserId: userId, // Store original ID for reference - }, + names, + metadata, }); if (!created) { logger.error( - `Failed to create user ${userName} for agent ${this.agentId}.` + `Failed to create user ${names[0]} for agent ${this.agentId}.` ); return null; } logger.success( - `User ${userName} created successfully for agent ${this.agentId}.` + `User ${names[0]} created successfully for agent ${this.agentId}.` ); } - return tenantSpecificUserId; + return userId; } async ensureParticipantInRoom(userId: UUID, roomId: UUID) { - // Always get the tenant-specific user ID using our helper method - const tenantSpecificUserId = this.generateTenantUserId(userId); - // Make sure entity exists in database before adding as participant const entity = await this.databaseAdapter.getEntityById( - tenantSpecificUserId, + userId, this.agentId ); - if (!entity) { - // get the room by room id - const room = await this.databaseAdapter.getRoom(roomId, this.agentId); - if (!room) { - throw new Error(`Room ${roomId} does not exist`); - } - - // get the source of the room - const source = room.source; - - // Create entity if it doesn't exist - const createdUserId = await this.getOrCreateUser( - userId, // Original ID will be transformed inside getOrCreateUser - userId === this.agentId - ? this.character.username || "Agent" - : `User${userId.substring(0, 8)}`, - userId === this.agentId - ? this.character.name || "Agent" - : `User${userId.substring(0, 8)}`, - source - ); - - if (!createdUserId) { - throw new Error(`Failed to create entity for user ${userId}`); - } - - // Verify the entity was created - const createdEntity = await this.databaseAdapter.getEntityById( - tenantSpecificUserId, - this.agentId - ); - if (!createdEntity) { - throw new Error(`Failed to create entity for user ${userId}`); - } + if(!entity) { + throw new Error(`User ${userId} not found`); } - // Get current participants const participants = await this.databaseAdapter.getParticipantsForRoom( roomId, @@ -993,17 +925,17 @@ export class AgentRuntime implements IAgentRuntime { ); // Only add if not already a participant - if (!participants.includes(tenantSpecificUserId)) { + if (!participants.includes(userId)) { // Add participant using the tenant-specific ID that now exists in the entities table const added = await this.databaseAdapter.addParticipant( - tenantSpecificUserId, + userId, roomId, this.agentId ); if (!added) { throw new Error( - `Failed to add participant ${tenantSpecificUserId} to room ${roomId}` + `Failed to add participant ${userId} to room ${roomId}` ); } @@ -1013,7 +945,7 @@ export class AgentRuntime implements IAgentRuntime { ); } else { logger.log( - `User ${tenantSpecificUserId} linked to room ${roomId} successfully.` + `User ${userId} linked to room ${roomId} successfully.` ); } } @@ -1045,15 +977,22 @@ export class AgentRuntime implements IAgentRuntime { } if (!worldId && serverId) { - worldId = stringToUuid(`${serverId}-${this.agentId}`); + worldId = createUniqueUuid(this, serverId); } + const names = [userScreenName, userName] + const metadata = { + [source]: { + name: userScreenName, + userName: userName, + }, + }; + // Get tenant-specific user ID and ensure the user exists const tenantSpecificUserId = await this.getOrCreateUser( userId, - userName ?? `User${userId}`, - userScreenName ?? `User${userId}`, - source + names, + metadata, ); if (!tenantSpecificUserId) { @@ -1198,12 +1137,6 @@ export class AgentRuntime implements IAgentRuntime { message: Memory, additionalKeys: { [key: string]: unknown } = {} ) { - // Convert user ID to tenant-specific ID if needed - const tenantSpecificUserId = - message.userId === this.agentId - ? message.userId - : this.generateTenantUserId(message.userId); - const { roomId } = message; const conversationLength = this.getConversationLength(); @@ -1231,7 +1164,7 @@ export class AgentRuntime implements IAgentRuntime { }); const senderName = actorsData?.find( - (actor: Actor) => actor.id === tenantSpecificUserId + (actor: Actor) => actor.id === message.userId )?.name; // TODO: We may wish to consolidate and just accept character.name here instead of the actor name @@ -1302,10 +1235,9 @@ export class AgentRuntime implements IAgentRuntime { return example .map((message) => { let messageString = - `${message.user}: ${message.content.text}` + - (message.content.action + `${message.user}: ${message.content.text}${message.content.action ? ` (action: ${message.content.action})` - : ""); + : ""}`; exampleNames.forEach((name, index) => { const placeholder = `{{user${index + 1}}}`; messageString = messageString.replaceAll(placeholder, name); @@ -1317,16 +1249,12 @@ export class AgentRuntime implements IAgentRuntime { .join("\n\n"); const getRecentInteractions = async ( - userA: UUID, - userB: UUID + sourceEntityId: UUID, + targetEntityId: UUID ): Promise => { - // Convert to tenant-specific ID if needed - const tenantUserA = - userA === this.agentId ? userA : this.generateTenantUserId(userA); - - // Find all rooms where userA and userB are participants + // Find all rooms where sourceEntityId and targetEntityId are participants const rooms = await this.databaseAdapter.getRoomsForParticipants( - [tenantUserA, userB], + [sourceEntityId, targetEntityId], this.agentId ); @@ -1420,7 +1348,7 @@ export class AgentRuntime implements IAgentRuntime { Math.floor(Math.random() * this.character.adjectives.length) ] : "", - knowledge: formattedKnowledge, + knowledge: addHeader("# Knowledge", formattedKnowledge), knowledgeData: knowledgeData, // Recent interactions between the sender and receiver, formatted as messages recentMessageInteractions: formattedMessageInteractions, @@ -1496,7 +1424,7 @@ export class AgentRuntime implements IAgentRuntime { // Agent runtime stuff senderName, - actors: actors && actors.length > 0 ? addHeader("# Actors", actors) : "", + actors: actors && actors.length > 0 ? addHeader("# Actors in the Room", actors) : "", actorsData, roomId, recentMessages: @@ -1710,7 +1638,9 @@ export class AgentRuntime implements IAgentRuntime { if (!model) { throw new Error(`No handler found for delegate type: ${modelClass}`); } - return await model(this, params); + + const response = await model(this, params); + return response; } registerEvent(event: string, handler: (params: any) => void) { diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index 7552b613ab1..2f37de00e8b 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -1,6 +1,6 @@ +import { createUniqueUuid } from "./entities"; import { logger } from "./logger"; -import { OnboardingSetting, IAgentRuntime, WorldSettings, OnboardingConfig, WorldData } from "./types"; -import { stringToUuid } from "./uuid"; +import type { IAgentRuntime, OnboardingConfig, OnboardingSetting, WorldData, WorldSettings } from "./types"; function createSettingFromConfig( configSetting: Omit @@ -29,7 +29,7 @@ export async function updateWorldSettings( worldSettings: WorldSettings ): Promise { try { - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, serverId); const world = await runtime.getWorld(worldId); if (!world) { @@ -63,7 +63,7 @@ export async function getWorldSettings( serverId: string ): Promise { try { - const worldId = stringToUuid(`${serverId}-${runtime.agentId}`); + const worldId = createUniqueUuid(runtime, serverId); const world = await runtime.getWorld(worldId); if (!world || !world.metadata?.settings) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2d1cc245269..4b1cc8efd5d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -55,14 +55,17 @@ export interface ConversationExample { * Represents an actor/participant in a conversation */ export interface Actor { + /** Unique identifier */ + id: UUID; + /** Display name */ name: string; - /** Username/handle */ - username: string; + /** All names for the actor */ + names: string[]; - /** Unique identifier */ - id: UUID; + /** Arbitrary data which can be displayed */ + data: any; } /** @@ -476,24 +479,39 @@ export interface Relationship { id: UUID; /** First user ID */ - userA: UUID; + sourceEntityId: UUID; /** Second user ID */ - userB: UUID; + targetEntityId: UUID; - /** Primary user ID */ - userId: UUID; + /** Agent ID */ + agentId: UUID; - /** Associated room ID */ - roomId: UUID; + /** Tags for filtering/categorizing relationships */ + tags: string[]; - /** Relationship status */ - status: string; + /** Additional metadata about the relationship */ + metadata: { + [key: string]: any + } /** Optional creation timestamp */ createdAt?: string; } +export interface Component { + id: UUID; + entityId: UUID; + agentId: UUID; + roomId: UUID; + worldId: UUID; + sourceEntityId: UUID; + type: string; + data: { + [key: string]: any; + }; +} + /** * Represents a user account */ @@ -509,6 +527,9 @@ export interface Entity { /** Agent ID this account is related to, for agents should be themselves */ agentId: UUID; + + /** Optional array of components */ + components?: Component[]; } /** @@ -529,6 +550,9 @@ export interface Room { /** Unique identifier */ id: UUID; + /** Room name */ + name: string; + /** Room participants */ participants: Participant[]; } @@ -758,14 +782,33 @@ export interface IDatabaseAdapter { updateAgent(agent: Agent): Promise; - /** Get account by ID */ + /** Get entity by ID */ getEntityById(userId: UUID, agentId: UUID): Promise; - /** Create new account */ + /** Get entities for room */ + getEntitiesForRoom(roomId: UUID, agentId: UUID, includeComponents?: boolean): Promise; + + /** Create new entity */ createEntity(entity: Entity): Promise; + /** Update entity */ updateEntity(entity: Entity): Promise; + /** Get component by ID */ + getComponent(entityId: UUID, type: string, worldId?: UUID, sourceEntityId?: UUID): Promise; + + /** Get all components for an entity */ + getComponents(entityId: UUID, worldId?: UUID, sourceEntityId?: UUID): Promise; + + /** Create component */ + createComponent(component: Component): Promise; + + /** Update component */ + updateComponent(component: Component): Promise; + + /** Delete component */ + deleteComponent(componentId: UUID): Promise; + /** Get memories matching criteria */ getMemories(params: { roomId: UUID; @@ -804,8 +847,6 @@ export interface IDatabaseAdapter { type: string; }): Promise; - getActorDetails(params: { roomId: UUID }): Promise; - updateGoalStatus(params: { goalId: UUID; status: GoalStatus }): Promise; searchMemories(params: { @@ -822,7 +863,7 @@ export interface IDatabaseAdapter { memory: Memory, tableName: string, unique?: boolean - ): Promise; + ): Promise; removeMemory(memoryId: UUID, tableName: string): Promise; @@ -885,6 +926,8 @@ export interface IDatabaseAdapter { getRoomsForParticipants(userIds: UUID[], agentId: UUID): Promise; + getRooms(worldId: UUID): Promise; + addParticipant(userId: UUID, roomId: UUID, agentId: UUID): Promise; removeParticipant(userId: UUID, roomId: UUID, agentId: UUID): Promise; @@ -906,17 +949,49 @@ export interface IDatabaseAdapter { state: "FOLLOWED" | "MUTED" | null ): Promise; - createRelationship(params: { userA: UUID; userB: UUID; agentId: UUID }): Promise; + /** + * Creates a new relationship between two entities. + * @param params Object containing the relationship details + * @returns Promise resolving to boolean indicating success + */ + createRelationship(params: { + sourceEntityId: UUID; + targetEntityId: UUID; + agentId: UUID; + tags?: string[]; + metadata?: { [key: string]: any }; + }): Promise; + /** + * Updates an existing relationship between two entities. + * @param relationship The relationship object with updated data + * @returns Promise resolving to void + */ + updateRelationship(relationship: Relationship): Promise; + + /** + * Retrieves a relationship between two entities if it exists. + * @param params Object containing the entity IDs and agent ID + * @returns Promise resolving to the Relationship object or null if not found + */ getRelationship(params: { - userA: UUID; - userB: UUID; + sourceEntityId: UUID; + targetEntityId: UUID; agentId: UUID; }): Promise; - getRelationships(params: { userId: UUID; agentId: UUID }): Promise; + /** + * Retrieves all relationships for a specific entity. + * @param params Object containing the user ID, agent ID and optional tags to filter by + * @returns Promise resolving to an array of Relationship objects + */ + getRelationships(params: { + userId: UUID; + agentId: UUID; + tags?: string[]; + }): Promise; - createCharacter(character: Character): Promise; + createCharacter(character: Character): Promise; listCharacters(): Promise; @@ -976,7 +1051,7 @@ export interface IMemoryManager { limit?: number; }): Promise; - createMemory(memory: Memory, unique?: boolean): Promise; + createMemory(memory: Memory, unique?: boolean): Promise; removeMemory(memoryId: UUID): Promise; @@ -1010,7 +1085,7 @@ export abstract class Service { public static getInstance(): T { if (!Service.instance) { - Service.instance = new (this as any)(); + Service.instance = new (Service as any)(); } return Service.instance as T; } @@ -1048,8 +1123,6 @@ export interface IAgentRuntime { getClient(name: string): ClientInstance | null; getAllClients(): Map; - generateTenantUserId(userId: UUID): UUID; - registerClientInterface(name: string, client: Client): void; registerClient(name: string, client: ClientInstance): void; @@ -1092,9 +1165,14 @@ export interface IAgentRuntime { getOrCreateUser( userId: UUID, - userName: string | null, - name: string | null, - source: string | null + names: string[], + metadata: { + [source: string]: { + name: string; + userName: string; + [key: string]: unknown; + }; + } ): Promise; registerProvider(provider: Provider): void; @@ -1426,10 +1504,16 @@ export interface TeePluginConfig { export interface Task { id?: UUID; name: string; + metadata?: { + options?: { + name: string; + description: string; + }[]; + }; description: string; roomId: UUID; tags: string[]; - handler: (runtime: IAgentRuntime) => Promise; + handler: (runtime: IAgentRuntime, options: { [key: string]: unknown }) => Promise; validate?: (runtime: IAgentRuntime, message: Memory, state: State) => Promise; } @@ -1489,4 +1573,50 @@ export interface OnboardingConfig { settings: { [key: string]: Omit; }; +} + +/** + * Send a direct message to a user + * @param runtime The agent runtime instance + * @param targetEntityId The ID of the user to send the message to + * @param source The platform/source to send on (e.g. telegram, discord) + * @param message The message content to send + * @param worldId The world ID context + */ +export async function sendDirectMessage( + runtime: IAgentRuntime, + targetEntityId: UUID, + source: string, + message: string, + worldId: UUID +): Promise { + const client = runtime.getClient(source); + if (!client) { + throw new Error(`No client found for source: ${source}`); + } + + await client.sendDirectMessage?.(targetEntityId, message, worldId); +} + +/** + * Send a message to a room + * @param runtime The agent runtime instance + * @param roomId The ID of the room to send to + * @param source The platform/source to send on (e.g. telegram, discord) + * @param message The message content to send + * @param worldId The world ID context + */ +export async function sendRoomMessage( + runtime: IAgentRuntime, + roomId: UUID, + source: string, + message: string, + worldId: UUID +): Promise { + const client = runtime.getClient(source); + if (!client) { + throw new Error(`No client found for source: ${source}`); + } + + await client.sendRoomMessage?.(roomId, message, worldId); } \ No newline at end of file diff --git a/packages/plugin-discord/src/actions/summarizeConversation.ts b/packages/plugin-discord/src/actions/summarizeConversation.ts index 143c24aab59..e1089773a7f 100644 --- a/packages/plugin-discord/src/actions/summarizeConversation.ts +++ b/packages/plugin-discord/src/actions/summarizeConversation.ts @@ -219,8 +219,6 @@ const summarizeAction = { return; } - console.log("dateRange", dateRange); - const { objective, start, end } = dateRange; // 2. get these memories from the database diff --git a/packages/plugin-discord/src/actions/voiceJoin.ts b/packages/plugin-discord/src/actions/voiceJoin.ts index 4916aeeb426..ec84ba56d4d 100644 --- a/packages/plugin-discord/src/actions/voiceJoin.ts +++ b/packages/plugin-discord/src/actions/voiceJoin.ts @@ -7,21 +7,21 @@ import { type State, ChannelType, composeContext, + createUniqueUuid, generateText, - HandlerCallback, + type HandlerCallback, logger, - ModelClass, - stringToUuid, + ModelClass } from "@elizaos/core"; import { type Channel, type Guild, - BaseGuildVoiceChannel, + type BaseGuildVoiceChannel, ChannelType as DiscordChannelType } from "discord.js"; -import { DiscordClient } from "../index.ts"; -import { VoiceManager } from "../voice.ts"; +import type { DiscordClient } from "../index.ts"; +import type { VoiceManager } from "../voice.ts"; export default { name: "JOIN_VOICE", @@ -133,9 +133,7 @@ export default { const members = guild?.members.cache; // get the member who's stringTouuid(id) === message userId - const member = members?.find((member) => stringToUuid(member.id) === message.userId); - - console.log("member", member); + const member = members?.find((member) => createUniqueUuid(runtime, member.id) === message.userId); if (member?.voice?.channel) { voiceManager.joinChannel(member?.voice?.channel as BaseGuildVoiceChannel); diff --git a/packages/plugin-discord/src/actions/voiceLeave.ts b/packages/plugin-discord/src/actions/voiceLeave.ts index a21cb7bb4f9..7764e704e20 100644 --- a/packages/plugin-discord/src/actions/voiceLeave.ts +++ b/packages/plugin-discord/src/actions/voiceLeave.ts @@ -13,8 +13,8 @@ import { BaseGuildVoiceChannel } from "discord.js"; -import { DiscordClient } from "../index.ts"; -import { VoiceManager } from "../voice.ts"; +import type { DiscordClient } from "../index.ts"; +import type { VoiceManager } from "../voice.ts"; export default { diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index f46ee11ec4e..83cd094f076 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -1,6 +1,7 @@ import { ChannelType, type Character, + createUniqueUuid, type Client as ElizaClient, type HandlerCallback, type IAgentRuntime, @@ -8,9 +9,8 @@ import { type Memory, type Plugin, RoleName, - stringToUuid, - UUID, - WorldData, + type UUID, + type WorldData } from "@elizaos/core"; import { Client, @@ -18,7 +18,7 @@ import { Events, GatewayIntentBits, type Guild, - GuildMember, + type GuildMember, type MessageReaction, type OAuth2Guild, Partials, @@ -103,16 +103,14 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { const guildChannels = await guild.fetch(); // for channel in channels for (const [, channel] of guildChannels.channels.cache) { - const roomId = stringToUuid(`${channel.id}-${runtime.agentId}`); + const roomId = createUniqueUuid(this.runtime, channel.id) const room = await runtime.getRoom(roomId); // if the room already exists, skip if (room) { continue; } - const worldId = stringToUuid(`${guild.id}-${runtime.agentId}`); - - const ownerId = stringToUuid(`${guildObj.ownerId}-${runtime.agentId}`); - const tenantSpecificOwnerId = runtime.generateTenantUserId(ownerId); + const worldId = createUniqueUuid(runtime, guild.id); + const ownerId = createUniqueUuid(this.runtime, guildObj.ownerId); await runtime.ensureWorldExists({ id: worldId, name: guild.name, @@ -121,7 +119,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { metadata: { ownership: guildObj.ownerId ? { ownerId } : undefined, roles: { - [tenantSpecificOwnerId]: RoleName.OWNER, + [ownerId]: RoleName.OWNER, }, }, }); @@ -187,6 +185,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { // Emit standardized USER_JOINED event this.runtime.emitEvent("USER_JOINED", { runtime: this.runtime, + entityId: createUniqueUuid(this.runtime, member.id), user: { id: member.id, username: tag, @@ -200,6 +199,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { this.runtime.emitEvent("DISCORD_USER_JOINED", { runtime: this.runtime, + entityId: createUniqueUuid(this.runtime, member.id), member, guild, }); @@ -316,13 +316,9 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { // Generate IDs with timestamp to ensure uniqueness const timestamp = Date.now(); - const roomId = stringToUuid( - `${reaction.message.channel.id}-${this.runtime.agentId}` - ); - const userIdUUID = stringToUuid(`${user.id}-${this.runtime.agentId}`); - const reactionUUID = stringToUuid( - `${reaction.message.id}-${user.id}-${emoji}-${timestamp}-${this.runtime.agentId}` - ); + const roomId = createUniqueUuid(this.runtime, reaction.message.channel.id) + const userIdUUID = createUniqueUuid(this.runtime, user.id); + const reactionUUID = createUniqueUuid(this.runtime, `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`); // Validate IDs if (!userIdUUID || !roomId) { @@ -358,6 +354,8 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { type: await this.getChannelType(reaction.message.channel.id), }); + const inReplyTo = createUniqueUuid(this.runtime, reaction.message.id); + const memory: Memory = { id: reactionUUID, userId: userIdUUID, @@ -367,9 +365,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { userName, text: reactionMessage, source: "discord", - inReplyTo: stringToUuid( - `${reaction.message.id}-${this.runtime.agentId}` - ), + inReplyTo, }, roomId, createdAt: timestamp, @@ -427,13 +423,11 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { const reactionMessage = `*Removed <${emoji}> from: "${truncatedContent}"*`; - const roomId = stringToUuid( - `${reaction.message.channel.id}-${this.runtime.agentId}` - ); - const userIdUUID = stringToUuid(`${user.id}-${this.runtime.agentId}`); - const reactionUUID = stringToUuid( - `${reaction.message.id}-${user.id}-${emoji}-removed-${this.runtime.agentId}` - ); + const roomId = createUniqueUuid(this.runtime, reaction.message.channel.id) + + const userIdUUID = createUniqueUuid(this.runtime, user.id); + const timestamp = Date.now(); + const reactionUUID = createUniqueUuid(this.runtime, `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`); const userName = reaction.message.author?.username || "unknown"; const name = reaction.message.author?.displayName || userName; @@ -458,9 +452,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { userName, text: reactionMessage, source: "discord", - inReplyTo: stringToUuid( - `${reaction.message.id}-${this.runtime.agentId}` - ), + inReplyTo: createUniqueUuid(this.runtime, reaction.message.id), }, roomId, createdAt: Date.now(), @@ -490,14 +482,11 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { const fullGuild = await guild.fetch(); this.voiceManager.scanGuild(guild); - const ownerId = stringToUuid( - `${fullGuild.ownerId}-${this.runtime.agentId}` - ); + const ownerId = createUniqueUuid(this.runtime, fullGuild.ownerId); // Create standardized world data structure - const worldId = stringToUuid(`${fullGuild.id}-${this.runtime.agentId}`); - const tenantSpecificOwnerId = this.runtime.generateTenantUserId(ownerId); - const standardizedData = { + const worldId = createUniqueUuid(this.runtime, fullGuild.id); + const standardizedData = { runtime: this.runtime, rooms: await this.buildStandardizedRooms(fullGuild, worldId), users: await this.buildStandardizedUsers(fullGuild), @@ -507,9 +496,9 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { agentId: this.runtime.agentId, serverId: fullGuild.id, metadata: { - ownership: fullGuild.ownerId ? { ownerId: tenantSpecificOwnerId } : undefined, + ownership: fullGuild.ownerId ? { ownerId: ownerId } : undefined, roles: { - [tenantSpecificOwnerId]: RoleName.OWNER, + [ownerId]: RoleName.OWNER, }, }, } as WorldData, @@ -545,7 +534,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { */ private async buildStandardizedRooms( guild: Guild, - worldId: UUID + _worldId: UUID ): Promise { const rooms = []; @@ -555,7 +544,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { channel.type === DiscordChannelType.GuildText || channel.type === DiscordChannelType.GuildVoice ) { - const roomId = stringToUuid(`${channelId}-${this.runtime.agentId}`); + const roomId = createUniqueUuid(this.runtime, channelId) let channelType; switch (channel.type) { @@ -587,7 +576,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { ?.has(PermissionsBitField.Flags.ViewChannel) ) .map((member) => - stringToUuid(`${member.id}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, member.id) ); } catch (error) { logger.warn( @@ -633,18 +622,24 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { if (member.id !== botId) { users.push({ - id: stringToUuid(`${member.id}-${this.runtime.agentId}`), + id: createUniqueUuid(this.runtime, member.id), names: Array.from( - new Set([member.user.username, member.displayName]) + new Set([member.user.username, member.displayName, member.user.globalName]) ), metadata: { default: { username: tag, name: member.displayName || member.user.username, }, - discord: { + discord: member.user.globalName ? { + username: tag, + name: member.displayName || member.user.username, + globalName: member.user.globalName, + userId: member.id, + } : { username: tag, - displayName: member.displayName || member.user.username, + name: member.displayName || member.user.username, + userId: member.id, }, }, }); @@ -659,9 +654,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { for (const [, member] of onlineMembers) { if (member.id !== botId) { - const userId = stringToUuid( - `${member.id}-${this.runtime.agentId}` - ); + const userId = createUniqueUuid(this.runtime, member.id); // Avoid duplicates if (!users.some((u) => u.id === userId)) { const tag = member.user.bot @@ -671,16 +664,22 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { users.push({ id: userId, names: Array.from( - new Set([member.user.username, member.displayName]) + new Set([member.user.username, member.displayName, member.user.globalName]) ), metadata: { - discord: { + default: { username: tag, - displayName: member.displayName || member.user.username, + name: member.displayName || member.user.username, }, - default: { + discord: member.user.globalName ? { username: tag, name: member.displayName || member.user.username, + globalName: member.user.globalName, + userId: member.id, + } : { + username: tag, + name: member.displayName || member.user.username, + userId: member.id, }, }, }); @@ -706,18 +705,24 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { : member.user.username; users.push({ - id: stringToUuid(`${member.id}-${this.runtime.agentId}`), + id: createUniqueUuid(this.runtime, member.id), names: Array.from( - new Set([member.user.username, member.displayName]) + new Set([member.user.username, member.displayName, member.user.globalName]) ), metadata: { default: { username: tag, name: member.displayName || member.user.username, }, - discord: { + discord: member.user.globalName ? { + username: tag, + name: member.displayName || member.user.username, + globalName: member.user.globalName, + userId: member.id, + } : { username: tag, - displayName: member.displayName || member.user.username, + name: member.displayName || member.user.username, + userId: member.id, }, }, }); @@ -752,12 +757,8 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { }); // Create platform-agnostic world data structure with simplified structure - const worldId = stringToUuid(`${fullGuild.id}-${this.runtime.agentId}`); - - const ownerId = stringToUuid( - `${fullGuild.ownerId}-${this.runtime.agentId}` - ); - const tenantSpecificOwnerId = this.runtime.generateTenantUserId(ownerId); + const worldId = createUniqueUuid(this.runtime, fullGuild.id); + const ownerId = createUniqueUuid(this.runtime, fullGuild.ownerId); const standardizedData = { runtime: this.runtime, @@ -771,7 +772,7 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { metadata: { ownership: fullGuild.ownerId ? { ownerId } : undefined, roles: { - [tenantSpecificOwnerId]: RoleName.OWNER, + [ownerId]: RoleName.OWNER, }, }, } as WorldData, diff --git a/packages/plugin-discord/src/messages.ts b/packages/plugin-discord/src/messages.ts index 76c9902dc17..a2be593a801 100644 --- a/packages/plugin-discord/src/messages.ts +++ b/packages/plugin-discord/src/messages.ts @@ -1,5 +1,7 @@ import { + ChannelType, type Content, + createUniqueUuid, type HandlerCallback, type IAgentRuntime, type IBrowserService, @@ -7,14 +9,11 @@ import { logger, type Media, type Memory, - ServiceType, - stringToUuid, - type UUID, - ChannelType, + ServiceType } from "@elizaos/core"; import { - ChannelType as DiscordChannelType, type Client, + ChannelType as DiscordChannelType, type Message as DiscordMessage, type TextChannel, } from "discord.js"; @@ -61,15 +60,14 @@ export class MessageManager { return; } - const userIdUUID = stringToUuid( - `${message.author.id}-${this.runtime.agentId}` - ); + const userIdUUID = createUniqueUuid(this.runtime, message.author.id); + const userName = message.author.bot ? `${message.author.username}#${message.author.discriminator}` : message.author.username; const name = message.author.displayName; const channelId = message.channel.id; - const roomId = stringToUuid(`${channelId}-${this.runtime.agentId}`); + const roomId = createUniqueUuid(this.runtime, channelId); let type: ChannelType; let serverId: string | undefined; @@ -122,11 +120,9 @@ export class MessageManager { return; } - const userIdUUID = stringToUuid( - `${message.author.id}-${this.runtime.agentId}` - ); + const userIdUUID = createUniqueUuid(this.runtime, message.author.id); - const messageId = stringToUuid(`${message.id}-${this.runtime.agentId}`); + const messageId = createUniqueUuid(this.runtime, message.id); const newMessage: Memory = { id: messageId, @@ -141,7 +137,7 @@ export class MessageManager { source: "discord", url: message.url, inReplyTo: message.reference?.messageId - ? stringToUuid(message.reference.messageId) + ? createUniqueUuid(this.runtime, message.reference?.messageId) : undefined, }, createdAt: message.createdTimestamp, @@ -153,9 +149,7 @@ export class MessageManager { ) => { try { if (message.id && !content.inReplyTo) { - content.inReplyTo = stringToUuid( - `${message.id}-${this.runtime.agentId}` - ); + content.inReplyTo = createUniqueUuid(this.runtime, message.id); } const messages = await sendMessageInChunks( message.channel as TextChannel, @@ -167,12 +161,9 @@ export class MessageManager { const memories: Memory[] = []; for (const m of messages) { let action = content.action; - if (messages.length > 1 && m !== messages[messages.length - 1]) { - action = "CONTINUE"; - } const memory: Memory = { - id: stringToUuid(`${m.id}-${this.runtime.agentId}`), + id: createUniqueUuid(this.runtime, m.id), userId: this.runtime.agentId, agentId: this.runtime.agentId, content: { @@ -322,7 +313,7 @@ export class MessageManager { const discriminator = data.discriminator; return ( (data as { username: string }).username + - (discriminator ? "#" + discriminator : "") + (discriminator ? `#${discriminator}` : "") ); } } diff --git a/packages/plugin-discord/src/providers/voiceState.ts b/packages/plugin-discord/src/providers/voiceState.ts index 8355f86bd0a..545bab86e5a 100644 --- a/packages/plugin-discord/src/providers/voiceState.ts +++ b/packages/plugin-discord/src/providers/voiceState.ts @@ -35,9 +35,9 @@ const voiceStateProvider: Provider = { throw new Error("No world found"); } - const worldName = world.name; + const _worldName = world.name; - const roomType = room.type; + const _roomType = room.type; const channelId = room.channelId diff --git a/packages/plugin-discord/src/voice.ts b/packages/plugin-discord/src/voice.ts index 56ef6bcc4bc..47d34f8e109 100644 --- a/packages/plugin-discord/src/voice.ts +++ b/packages/plugin-discord/src/voice.ts @@ -12,28 +12,28 @@ import { joinVoiceChannel, } from "@discordjs/voice"; import { + type ChannelType, type Content, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, type UUID, - logger, - stringToUuid, - type ChannelType + createUniqueUuid, + logger } from "@elizaos/core"; import { type BaseGuildVoiceChannel, - ChannelType as DiscordChannelType, type Client, + ChannelType as DiscordChannelType, type Guild, type GuildMember, type VoiceChannel, type VoiceState, } from "discord.js"; import EventEmitter from "node:events"; -import prism from "prism-media"; import { type Readable, pipeline } from "node:stream"; +import prism from "prism-media"; import type { DiscordClient } from "./index.ts"; import { getWavHeader } from "./utils.ts"; @@ -635,8 +635,8 @@ export class VoiceManager extends EventEmitter { return { text: "", action: "IGNORE" }; } - const roomId = stringToUuid(`${channelId}-${this.runtime.agentId}`); - const userIdUUID = stringToUuid(`${userId}-${this.runtime.agentId}`); + const roomId = createUniqueUuid(this.runtime, channelId); + const userIdUUID = createUniqueUuid(this.runtime, userId); const guild = await channel.guild.fetch(); const type = await this.getChannelType(guild.id); @@ -652,7 +652,7 @@ export class VoiceManager extends EventEmitter { }); const memory: Memory = { - id: stringToUuid(`${channelId}-voice-message-${Date.now()}`), + id: createUniqueUuid(this.runtime, `${channelId}-voice-message-${Date.now()}`), agentId: this.runtime.agentId, userId: userIdUUID, roomId, @@ -670,7 +670,7 @@ export class VoiceManager extends EventEmitter { const callback: HandlerCallback = async (content: Content, _files: any[] = []) => { try { const responseMemory: Memory = { - id: stringToUuid(`${memory.id}-voice-response-${Date.now()}`), + id: createUniqueUuid(this.runtime, `${memory.id}-voice-response-${Date.now()}`), userId: this.runtime.agentId, agentId: this.runtime.agentId, content: { diff --git a/packages/plugin-sql/drizzle/migrations/20250225233329_init.sql b/packages/plugin-sql/drizzle/migrations/20250227085330_init.sql similarity index 79% rename from packages/plugin-sql/drizzle/migrations/20250225233329_init.sql rename to packages/plugin-sql/drizzle/migrations/20250227085330_init.sql index 1977dc7735a..de6d1ea7d51 100644 --- a/packages/plugin-sql/drizzle/migrations/20250225233329_init.sql +++ b/packages/plugin-sql/drizzle/migrations/20250227085330_init.sql @@ -8,7 +8,7 @@ CREATE TABLE "agents" ( CREATE TABLE "cache" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "key" text NOT NULL, - "agentId" text NOT NULL, + "agentId" uuid NOT NULL, "value" jsonb DEFAULT '{}'::jsonb, "createdAt" timestamptz DEFAULT now() NOT NULL, "expiresAt" timestamptz, @@ -33,6 +33,18 @@ CREATE TABLE "characters" ( "created_at" timestamptz DEFAULT now() ); --> statement-breakpoint +CREATE TABLE "components" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "entityId" uuid NOT NULL, + "agentId" uuid NOT NULL, + "roomId" uuid NOT NULL, + "worldId" uuid, + "sourceEntityId" uuid, + "type" text NOT NULL, + "data" jsonb DEFAULT '{}'::jsonb, + "createdAt" timestamptz DEFAULT now() NOT NULL +); +--> statement-breakpoint CREATE TABLE "embeddings" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "memory_id" uuid, @@ -115,11 +127,12 @@ CREATE TABLE "participants" ( CREATE TABLE "relationships" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "createdAt" timestamptz DEFAULT now() NOT NULL, - "userA" uuid NOT NULL, - "userB" uuid NOT NULL, + "sourceEntityId" uuid NOT NULL, + "targetEntityId" uuid NOT NULL, "agentId" uuid NOT NULL, - "status" text, - "userId" uuid NOT NULL + "tags" text[], + "metadata" jsonb, + CONSTRAINT "unique_relationship" UNIQUE("sourceEntityId","targetEntityId","agentId") ); --> statement-breakpoint CREATE TABLE "rooms" ( @@ -145,8 +158,15 @@ CREATE TABLE "worlds" ( ); --> statement-breakpoint ALTER TABLE "agents" ADD CONSTRAINT "agents_characterId_characters_id_fk" FOREIGN KEY ("characterId") REFERENCES "public"."characters"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cache" ADD CONSTRAINT "cache_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "components" ADD CONSTRAINT "components_entityId_entities_id_fk" FOREIGN KEY ("entityId") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "components" ADD CONSTRAINT "components_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "components" ADD CONSTRAINT "components_roomId_rooms_id_fk" FOREIGN KEY ("roomId") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "components" ADD CONSTRAINT "components_worldId_worlds_id_fk" FOREIGN KEY ("worldId") REFERENCES "public"."worlds"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "components" ADD CONSTRAINT "components_sourceEntityId_entities_id_fk" FOREIGN KEY ("sourceEntityId") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "embeddings" ADD CONSTRAINT "embeddings_memory_id_memories_id_fk" FOREIGN KEY ("memory_id") REFERENCES "public"."memories"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "embeddings" ADD CONSTRAINT "fk_embedding_memory" FOREIGN KEY ("memory_id") REFERENCES "public"."memories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "entities" ADD CONSTRAINT "entities_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "goals" ADD CONSTRAINT "goals_userId_entities_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "goals" ADD CONSTRAINT "goals_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "goals" ADD CONSTRAINT "goals_roomId_rooms_id_fk" FOREIGN KEY ("roomId") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint @@ -167,15 +187,14 @@ ALTER TABLE "participants" ADD CONSTRAINT "participants_roomId_rooms_id_fk" FORE ALTER TABLE "participants" ADD CONSTRAINT "participants_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "participants" ADD CONSTRAINT "fk_room" FOREIGN KEY ("roomId") REFERENCES "public"."rooms"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "participants" ADD CONSTRAINT "fk_user" FOREIGN KEY ("userId") REFERENCES "public"."entities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "relationships" ADD CONSTRAINT "relationships_userA_entities_id_fk" FOREIGN KEY ("userA") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "relationships" ADD CONSTRAINT "relationships_userB_entities_id_fk" FOREIGN KEY ("userB") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relationships" ADD CONSTRAINT "relationships_sourceEntityId_entities_id_fk" FOREIGN KEY ("sourceEntityId") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relationships" ADD CONSTRAINT "relationships_targetEntityId_entities_id_fk" FOREIGN KEY ("targetEntityId") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "relationships" ADD CONSTRAINT "relationships_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "relationships" ADD CONSTRAINT "relationships_userId_entities_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."entities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "relationships" ADD CONSTRAINT "fk_user_a" FOREIGN KEY ("userA") REFERENCES "public"."entities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "relationships" ADD CONSTRAINT "fk_user_b" FOREIGN KEY ("userB") REFERENCES "public"."entities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "relationships" ADD CONSTRAINT "fk_user" FOREIGN KEY ("userId") REFERENCES "public"."entities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relationships" ADD CONSTRAINT "fk_user_a" FOREIGN KEY ("sourceEntityId") REFERENCES "public"."entities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relationships" ADD CONSTRAINT "fk_user_b" FOREIGN KEY ("targetEntityId") REFERENCES "public"."entities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "rooms" ADD CONSTRAINT "rooms_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "rooms" ADD CONSTRAINT "rooms_worldId_worlds_id_fk" FOREIGN KEY ("worldId") REFERENCES "public"."worlds"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "worlds" ADD CONSTRAINT "worlds_agentId_agents_id_fk" FOREIGN KEY ("agentId") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint CREATE INDEX "idx_embedding_memory" ON "embeddings" USING btree ("memory_id");--> statement-breakpoint CREATE INDEX "idx_memories_type_room" ON "memories" USING btree ("type","roomId");--> statement-breakpoint CREATE INDEX "idx_memories_metadata_type" ON "memories" USING btree (((metadata->>'type')));--> statement-breakpoint @@ -183,7 +202,7 @@ CREATE INDEX "idx_memories_document_id" ON "memories" USING btree (((metadata->> CREATE INDEX "idx_fragments_order" ON "memories" USING btree (((metadata->>'documentId')),((metadata->>'position')));--> statement-breakpoint CREATE INDEX "idx_participants_user" ON "participants" USING btree ("userId");--> statement-breakpoint CREATE INDEX "idx_participants_room" ON "participants" USING btree ("roomId");--> statement-breakpoint -CREATE INDEX "idx_relationships_users" ON "relationships" USING btree ("userA","userB"); +CREATE INDEX "idx_relationships_users" ON "relationships" USING btree ("sourceEntityId","targetEntityId"); CREATE EXTENSION IF NOT EXISTS vector; --> statement-breakpoint diff --git a/packages/plugin-sql/drizzle/migrations/meta/20250225233329_snapshot.json b/packages/plugin-sql/drizzle/migrations/meta/20250227085330_snapshot.json similarity index 86% rename from packages/plugin-sql/drizzle/migrations/meta/20250225233329_snapshot.json rename to packages/plugin-sql/drizzle/migrations/meta/20250227085330_snapshot.json index 81e0e8cbedf..4bbaaeb3bbd 100644 --- a/packages/plugin-sql/drizzle/migrations/meta/20250225233329_snapshot.json +++ b/packages/plugin-sql/drizzle/migrations/meta/20250227085330_snapshot.json @@ -1,5 +1,5 @@ { - "id": "fe6cf216-3ff6-4d2c-866e-9f132bc12f13", + "id": "c737db6d-154c-40a5-bdf2-361303475c68", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -77,7 +77,7 @@ }, "agentId": { "name": "agentId", - "type": "text", + "type": "uuid", "primaryKey": false, "notNull": true }, @@ -103,7 +103,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "cache_agentId_agents_id_fk": { + "name": "cache_agentId_agents_id_fk", + "tableFrom": "cache", + "tableTo": "agents", + "columnsFrom": [ + "agentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": { "cache_key_agent_unique": { @@ -233,6 +247,142 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.components": { + "name": "components", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entityId": { + "name": "entityId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agentId": { + "name": "agentId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "roomId": { + "name": "roomId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "worldId": { + "name": "worldId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sourceEntityId": { + "name": "sourceEntityId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamptz", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "components_entityId_entities_id_fk": { + "name": "components_entityId_entities_id_fk", + "tableFrom": "components", + "tableTo": "entities", + "columnsFrom": [ + "entityId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "components_agentId_agents_id_fk": { + "name": "components_agentId_agents_id_fk", + "tableFrom": "components", + "tableTo": "agents", + "columnsFrom": [ + "agentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "components_roomId_rooms_id_fk": { + "name": "components_roomId_rooms_id_fk", + "tableFrom": "components", + "tableTo": "rooms", + "columnsFrom": [ + "roomId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "components_worldId_worlds_id_fk": { + "name": "components_worldId_worlds_id_fk", + "tableFrom": "components", + "tableTo": "worlds", + "columnsFrom": [ + "worldId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "components_sourceEntityId_entities_id_fk": { + "name": "components_sourceEntityId_entities_id_fk", + "tableFrom": "components", + "tableTo": "entities", + "columnsFrom": [ + "sourceEntityId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.embeddings": { "name": "embeddings", "schema": "", @@ -389,7 +539,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "entities_agentId_agents_id_fk": { + "name": "entities_agentId_agents_id_fk", + "tableFrom": "entities", + "tableTo": "agents", + "columnsFrom": [ + "agentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": { "id_agent_id_unique": { @@ -1042,14 +1206,14 @@ "notNull": true, "default": "now()" }, - "userA": { - "name": "userA", + "sourceEntityId": { + "name": "sourceEntityId", "type": "uuid", "primaryKey": false, "notNull": true }, - "userB": { - "name": "userB", + "targetEntityId": { + "name": "targetEntityId", "type": "uuid", "primaryKey": false, "notNull": true @@ -1060,17 +1224,17 @@ "primaryKey": false, "notNull": true }, - "status": { - "name": "status", - "type": "text", + "tags": { + "name": "tags", + "type": "text[]", "primaryKey": false, "notNull": false }, - "userId": { - "name": "userId", - "type": "uuid", + "metadata": { + "name": "metadata", + "type": "jsonb", "primaryKey": false, - "notNull": true + "notNull": false } }, "indexes": { @@ -1078,13 +1242,13 @@ "name": "idx_relationships_users", "columns": [ { - "expression": "userA", + "expression": "sourceEntityId", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "userB", + "expression": "targetEntityId", "isExpression": false, "asc": true, "nulls": "last" @@ -1097,12 +1261,12 @@ } }, "foreignKeys": { - "relationships_userA_entities_id_fk": { - "name": "relationships_userA_entities_id_fk", + "relationships_sourceEntityId_entities_id_fk": { + "name": "relationships_sourceEntityId_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", "columnsFrom": [ - "userA" + "sourceEntityId" ], "columnsTo": [ "id" @@ -1110,12 +1274,12 @@ "onDelete": "no action", "onUpdate": "no action" }, - "relationships_userB_entities_id_fk": { - "name": "relationships_userB_entities_id_fk", + "relationships_targetEntityId_entities_id_fk": { + "name": "relationships_targetEntityId_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", "columnsFrom": [ - "userB" + "targetEntityId" ], "columnsTo": [ "id" @@ -1136,25 +1300,12 @@ "onDelete": "no action", "onUpdate": "no action" }, - "relationships_userId_entities_id_fk": { - "name": "relationships_userId_entities_id_fk", - "tableFrom": "relationships", - "tableTo": "entities", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, "fk_user_a": { "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", "columnsFrom": [ - "userA" + "sourceEntityId" ], "columnsTo": [ "id" @@ -1167,20 +1318,7 @@ "tableFrom": "relationships", "tableTo": "entities", "columnsFrom": [ - "userB" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "fk_user": { - "name": "fk_user", - "tableFrom": "relationships", - "tableTo": "entities", - "columnsFrom": [ - "userId" + "targetEntityId" ], "columnsTo": [ "id" @@ -1190,7 +1328,17 @@ } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {}, + "uniqueConstraints": { + "unique_relationship": { + "name": "unique_relationship", + "nullsNotDistinct": false, + "columns": [ + "sourceEntityId", + "targetEntityId", + "agentId" + ] + } + }, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false @@ -1341,7 +1489,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "worlds_agentId_agents_id_fk": { + "name": "worlds_agentId_agents_id_fk", + "tableFrom": "worlds", + "tableTo": "agents", + "columnsFrom": [ + "agentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, diff --git a/packages/plugin-sql/drizzle/migrations/meta/_journal.json b/packages/plugin-sql/drizzle/migrations/meta/_journal.json index 7b971e9883e..d6c124f8f44 100644 --- a/packages/plugin-sql/drizzle/migrations/meta/_journal.json +++ b/packages/plugin-sql/drizzle/migrations/meta/_journal.json @@ -5,8 +5,15 @@ { "idx": 0, "version": "7", - "when": 1740526409252, - "tag": "20250225233329_init", + "when": 1740646410010, + "tag": "20250227085330_init", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1740738663301, + "tag": "20250228103103_big_kitty_pryde", "breakpoints": true } ] diff --git a/packages/plugin-sql/src/base.ts b/packages/plugin-sql/src/base.ts index 12e51b44545..30de73e408b 100644 --- a/packages/plugin-sql/src/base.ts +++ b/packages/plugin-sql/src/base.ts @@ -1,6 +1,7 @@ import { Actor, - Agent, + type Agent, + type Component, DatabaseAdapter, logger, type Character, @@ -42,11 +43,11 @@ import { import { v4 } from "uuid"; import { characterToInsert, - StoredTemplate, + type StoredTemplate, storedToTemplate, templateToStored, } from "./schema/character"; -import { DIMENSION_MAP, EmbeddingDimensionColumn } from "./schema/embedding"; +import { DIMENSION_MAP, type EmbeddingDimensionColumn } from "./schema/embedding"; import { cacheTable, characterTable, @@ -60,8 +61,9 @@ import { relationshipTable, roomTable, worldTable, + componentTable, } from "./schema/index"; -import { DrizzleOperations } from "./types"; +import type { DrizzleOperations } from "./types"; export abstract class BaseDrizzleAdapter extends DatabaseAdapter @@ -213,16 +215,84 @@ export abstract class BaseDrizzleAdapter async getEntityById(userId: UUID, agentId: UUID): Promise { return this.withDatabase(async () => { const result = await this.db - .select() + .select({ + entity: entityTable, + components: componentTable + }) .from(entityTable) - .where(and(eq(entityTable.id, userId), eq(entityTable.agentId, agentId))) - .limit(1); + .leftJoin( + componentTable, + eq(componentTable.entityId, entityTable.id) + ) + .where( + and( + eq(entityTable.id, userId), + eq(entityTable.agentId, agentId) + ) + ); if (result.length === 0) return null; - const account = result[0]; + // Group components by entity + const entity = result[0].entity; + entity.components = result + .filter(row => row.components) + .map(row => row.components); + + return entity; + }); + } + + async getEntitiesForRoom(roomId: UUID, agentId: UUID, includeComponents?: boolean): Promise { + return this.withDatabase(async () => { + const query = this.db + .select({ + entity: entityTable, + ...(includeComponents && { components: componentTable }) + }) + .from(participantTable) + .leftJoin( + entityTable, + and( + eq(participantTable.userId, entityTable.id), + eq(entityTable.agentId, agentId) + ) + ); + + if (includeComponents) { + query.leftJoin( + componentTable, + eq(componentTable.entityId, entityTable.id) + ); + } + + const result = await query.where(eq(participantTable.roomId, roomId)); + + // Group components by entity if includeComponents is true + const entitiesByIdMap = new Map(); + + result.forEach(row => { + if (!row.entity) return; + + const entityId = row.entity.id as UUID; + if (!entitiesByIdMap.has(entityId)) { + const entity: Entity = { + ...row.entity, + components: includeComponents ? [] : undefined + }; + entitiesByIdMap.set(entityId, entity); + } + + if (includeComponents && row.components) { + const entity = entitiesByIdMap.get(entityId)!; + if (!entity.components) { + entity.components = []; + } + entity.components.push(row.components); + } + }); - return account; + return Array.from(entitiesByIdMap.values()); }); } @@ -255,6 +325,78 @@ export abstract class BaseDrizzleAdapter }); } + async getComponent(entityId: UUID, type: string, worldId?: UUID, sourceEntityId?: UUID): Promise { + return this.withDatabase(async () => { + const conditions = [ + eq(componentTable.entityId, entityId), + eq(componentTable.type, type) + ]; + + if (worldId) { + conditions.push(eq(componentTable.worldId, worldId)); + } + + if (sourceEntityId) { + conditions.push(eq(componentTable.sourceEntityId, sourceEntityId)); + } + + const result = await this.db + .select() + .from(componentTable) + .where(and(...conditions)); + return result.length > 0 ? result[0] : null; + }); + } + + async getComponents(entityId: UUID, worldId?: UUID, sourceEntityId?: UUID): Promise { + return this.withDatabase(async () => { + const conditions = [ + eq(componentTable.entityId, entityId) + ]; + + if (worldId) { + conditions.push(eq(componentTable.worldId, worldId)); + } + + if (sourceEntityId) { + conditions.push(eq(componentTable.sourceEntityId, sourceEntityId)); + } + + const result = await this.db + .select({ + id: componentTable.id, + entityId: componentTable.entityId, + type: componentTable.type, + data: componentTable.data, + worldId: componentTable.worldId, + sourceEntityId: componentTable.sourceEntityId, + createdAt: componentTable.createdAt, + }) + .from(componentTable) + .where(and(...conditions)); + return result; + }); + } + + async createComponent(component: Component): Promise { + return this.withDatabase(async () => { + await this.db.insert(componentTable).values(component); + return true; + }); + } + + async updateComponent(component: Component): Promise { + return this.withDatabase(async () => { + await this.db.update(componentTable).set(component).where(eq(componentTable.id, component.id)); + }); + } + + async deleteComponent(componentId: UUID): Promise { + return this.withDatabase(async () => { + await this.db.delete(componentTable).where(eq(componentTable.id, componentId)); + }); + } + async getMemories(params: { roomId: UUID; count?: number; @@ -683,85 +825,7 @@ export abstract class BaseDrizzleAdapter }); } - async getActorDetails(params: { roomId: string }): Promise { - if (!params.roomId) { - throw new Error("roomId is required"); - } - - return this.withDatabase(async () => { - try { - const result = await this.db - .select({ - id: entityTable.id, - metadata: entityTable.metadata, - }) - .from(participantTable) - .leftJoin( - entityTable, - eq(participantTable.userId, entityTable.id) - ) - .where(eq(participantTable.roomId, params.roomId)) - .orderBy(entityTable.metadata?.name ?? entityTable.id); - - logger.debug("Retrieved actor details:", { - roomId: params.roomId, - actorCount: result.length, - }); - - return result.map((row) => { - try { - const details = - typeof row.details === "string" - ? JSON.parse(row.details) - : row.details || {}; - - return { - id: row.id as UUID, - name: row.name ?? "", - username: row.username ?? "", - details: { - tagline: details.tagline ?? "", - summary: details.summary ?? "", - quote: details.quote ?? "", - }, - }; - } catch (error) { - logger.warn("Failed to parse actor details:", { - actorId: row.id, - error: - error instanceof Error - ? error.message - : String(error), - }); - - return { - id: row.id as UUID, - name: row.name ?? "", - username: row.username ?? "", - details: { - tagline: "", - summary: "", - quote: "", - }, - }; - } - }); - } catch (error) { - logger.error("Failed to fetch actor details:", { - roomId: params.roomId, - error: - error instanceof Error ? error.message : String(error), - }); - throw new Error( - `Failed to fetch actor details: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } - }); - } - - async createMemory(memory: Memory & { metadata?: KnowledgeMetadata }, tableName: string): Promise { + async createMemory(memory: Memory & { metadata?: KnowledgeMetadata }, tableName: string): Promise { logger.debug("DrizzleAdapter createMemory:", { memoryId: memory.id, embeddingLength: memory.embedding?.length, @@ -788,7 +852,7 @@ export abstract class BaseDrizzleAdapter ? JSON.parse(memory.content) : memory.content; - const memoryId = memory.id ?? v4(); + const memoryId = memory.id ?? v4() as UUID; await this.db.transaction(async (tx) => { await tx.insert(memoryTable).values([{ @@ -819,6 +883,8 @@ export abstract class BaseDrizzleAdapter await tx.insert(embeddingTable).values([embeddingValues]); } }); + + return memoryId; } async removeMemory(memoryId: UUID, tableName: string): Promise { @@ -1053,6 +1119,16 @@ export abstract class BaseDrizzleAdapter }); } + async getRooms(worldId: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select() + .from(roomTable) + .where(eq(roomTable.worldId, worldId)); + return result; + }); + } + async updateRoom(room: RoomData): Promise { return this.withDatabase(async () => { await this.db.update(roomTable).set(room).where(eq(roomTable.id, room.id)); @@ -1267,135 +1343,135 @@ export abstract class BaseDrizzleAdapter } async createRelationship(params: { - userA: UUID; - userB: UUID; + sourceEntityId: UUID; + targetEntityId: UUID; + agentId: UUID; + tags?: string[]; + metadata?: { [key: string]: any }; }): Promise { - if (!params.userA || !params.userB) { - throw new Error("userA and userB are required"); - } - return this.withDatabase(async () => { + console.trace() try { - return await this.db.transaction(async (tx) => { - const relationshipId = v4(); - await tx.insert(relationshipTable).values({ - id: relationshipId, - userA: params.userA, - userB: params.userB, - userId: params.userA, - }); - - logger.debug("Relationship created successfully:", { - relationshipId, - userA: params.userA, - userB: params.userB, - }); - - return true; + const id = v4(); + await this.db.insert(relationshipTable).values({ + id, + sourceEntityId: params.sourceEntityId, + targetEntityId: params.targetEntityId, + agentId: params.agentId, + tags: params.tags || [], + metadata: params.metadata || {}, }); + return true; } catch (error) { - if ((error as { code?: string }).code === "23505") { - logger.warn("Relationship already exists:", { - userA: params.userA, - userB: params.userB, - error: - error instanceof Error - ? error.message - : String(error), - }); - } else { - logger.error("Failed to create relationship:", { - userA: params.userA, - userB: params.userB, - error: - error instanceof Error - ? error.message - : String(error), - }); - } + logger.error("Error creating relationship:", { + error: error instanceof Error ? error.message : String(error), + params, + }); return false; } }); } + async updateRelationship(relationship: Relationship): Promise { + return this.withDatabase(async () => { + try { + await this.db.update(relationshipTable) + .set({ + tags: relationship.tags || [], + metadata: relationship.metadata || {}, + }) + .where(eq(relationshipTable.id, relationship.id)); + } catch (error) { + logger.error("Error updating relationship:", { + error: error instanceof Error ? error.message : String(error), + relationship, + }); + throw error; + } + }); + } + async getRelationship(params: { - userA: UUID; - userB: UUID; + sourceEntityId: UUID; + targetEntityId: UUID; + agentId: UUID; }): Promise { - if (!params.userA || !params.userB) { - throw new Error("userA and userB are required"); - } - return this.withDatabase(async () => { try { const result = await this.db .select() .from(relationshipTable) .where( - or( - and( - eq(relationshipTable.userA, params.userA), - eq(relationshipTable.userB, params.userB) - ), - and( - eq(relationshipTable.userA, params.userB), - eq(relationshipTable.userB, params.userA) - ) + and( + eq(relationshipTable.sourceEntityId, params.sourceEntityId), + eq(relationshipTable.targetEntityId, params.targetEntityId), + eq(relationshipTable.agentId, params.agentId) ) ) .limit(1); - if (result.length > 0) { - return result[0] as unknown as Relationship; + if (result.length === 0) { + return null; } - logger.debug("No relationship found between users:", { - userA: params.userA, - userB: params.userB, - }); - return null; + return { + id: result[0].id, + sourceEntityId: result[0].sourceEntityId, + targetEntityId: result[0].targetEntityId, + agentId: result[0].agentId, + tags: result[0].tags || [], + metadata: result[0].metadata || {}, + createdAt: result[0].createdAt?.toString() + }; } catch (error) { - logger.error("Error fetching relationship:", { - userA: params.userA, - userB: params.userB, - error: - error instanceof Error ? error.message : String(error), + logger.error("Error getting relationship:", { + error: error instanceof Error ? error.message : String(error), + params, }); - throw error; + return null; } }); } - async getRelationships(params: { userId: UUID }): Promise { - if (!params.userId) { - throw new Error("userId is required"); - } + async getRelationships(params: { + userId: UUID; + agentId: UUID; + tags?: string[]; + }): Promise { return this.withDatabase(async () => { try { - const result = await this.db + let query = this.db .select() .from(relationshipTable) .where( - or( - eq(relationshipTable.userA, params.userId), - eq(relationshipTable.userB, params.userId) + and( + eq(relationshipTable.sourceEntityId, params.userId), + eq(relationshipTable.agentId, params.agentId) ) - ) - .orderBy(desc(relationshipTable.createdAt)); + ); - logger.debug("Retrieved relationships:", { - userId: params.userId, - count: result.length, - }); + // Filter by tags if provided + if (params.tags && params.tags.length > 0) { + query = query.where(sql`${relationshipTable.tags} && ARRAY[${sql.join(params.tags)}]::text[]`); + } - return result as unknown as Relationship[]; + const results = await query; + + return results.map(result => ({ + id: result.id, + sourceEntityId: result.sourceEntityId, + targetEntityId: result.targetEntityId, + agentId: result.agentId, + tags: result.tags || [], + metadata: result.metadata || {}, + createdAt: result.createdAt?.toString() + })); } catch (error) { - logger.error("Failed to fetch relationships:", { - userId: params.userId, - error: - error instanceof Error ? error.message : String(error), + logger.error("Error getting relationships:", { + error: error instanceof Error ? error.message : String(error), + params, }); - throw error; + return []; } }); } @@ -1492,7 +1568,7 @@ export abstract class BaseDrizzleAdapter } }); } - async createCharacter(character: Character): Promise { + async createCharacter(character: Character): Promise { return this.withDatabase(async () => { try { await this.db.transaction(async (tx) => { diff --git a/packages/plugin-sql/src/pg-lite/manager.ts b/packages/plugin-sql/src/pg-lite/manager.ts index ff78cb4370c..63486efc104 100644 --- a/packages/plugin-sql/src/pg-lite/manager.ts +++ b/packages/plugin-sql/src/pg-lite/manager.ts @@ -118,7 +118,7 @@ export class PGliteClientManager implements IDatabaseClientManager { logger.info("Migrations completed successfully!"); } catch (error) { logger.error("Failed to run database migrations:", error); - throw error; + // throw error; } } } diff --git a/packages/plugin-sql/src/pg/manager.ts b/packages/plugin-sql/src/pg/manager.ts index 37d9018f2c0..bc2ef05f0f3 100644 --- a/packages/plugin-sql/src/pg/manager.ts +++ b/packages/plugin-sql/src/pg/manager.ts @@ -178,7 +178,7 @@ export class PostgresConnectionManager implements IDatabaseClientManager logger.info("Migrations completed successfully!"); } catch (error) { logger.error("Failed to run database migrations:", error); - throw error; + // throw error; } } } diff --git a/packages/plugin-sql/src/schema/component.ts b/packages/plugin-sql/src/schema/component.ts new file mode 100644 index 00000000000..da1fdfab9ee --- /dev/null +++ b/packages/plugin-sql/src/schema/component.ts @@ -0,0 +1,21 @@ +import { pgTable, uuid, jsonb, text } from "drizzle-orm/pg-core"; +import { sql } from "drizzle-orm"; +import { entityTable } from "./entity"; +import { numberTimestamp } from "./types"; +import { agentTable } from "./agent"; +import { roomTable } from "./room"; +import { worldTable } from "./worldTable"; + +export const componentTable = pgTable("components", { + id: uuid("id").primaryKey().defaultRandom(), + entityId: uuid("entityId").notNull().references(() => entityTable.id), + agentId: uuid("agentId").notNull().references(() => agentTable.id), + roomId: uuid("roomId").notNull().references(() => roomTable.id), + worldId: uuid("worldId").references(() => worldTable.id), + sourceEntityId: uuid("sourceEntityId").references(() => entityTable.id), + type: text("type").notNull(), + data: jsonb("data").default(sql`'{}'::jsonb`), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), +}); \ No newline at end of file diff --git a/packages/plugin-sql/src/schema/index.ts b/packages/plugin-sql/src/schema/index.ts index e0c40b885aa..40359e8c356 100644 --- a/packages/plugin-sql/src/schema/index.ts +++ b/packages/plugin-sql/src/schema/index.ts @@ -1,12 +1,13 @@ -export { entityTable } from "./entity"; export { agentTable } from "./agent"; export { cacheTable } from "./cache"; export { characterTable } from "./character"; +export { componentTable } from "./component"; export { embeddingTable } from "./embedding"; +export { entityTable } from "./entity"; export { goalTable } from "./goal"; export { logTable } from "./log"; export { memoryTable } from "./memory"; export { participantTable } from "./participant"; export { relationshipTable } from "./relationship"; export { roomTable } from "./room"; -export { worldTable } from "./worldTable"; \ No newline at end of file +export { worldTable } from "./worldTable"; diff --git a/packages/plugin-sql/src/schema/relationship.ts b/packages/plugin-sql/src/schema/relationship.ts index 3227b36d1cd..dd0610317f8 100644 --- a/packages/plugin-sql/src/schema/relationship.ts +++ b/packages/plugin-sql/src/schema/relationship.ts @@ -4,6 +4,8 @@ import { text, index, foreignKey, + jsonb, + unique, } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; import { numberTimestamp } from "./types"; @@ -17,35 +19,29 @@ export const relationshipTable = pgTable( createdAt: numberTimestamp("createdAt") .default(sql`now()`) .notNull(), - userA: uuid("userA") + sourceEntityId: uuid("sourceEntityId") .notNull() .references(() => entityTable.id), - userB: uuid("userB") + targetEntityId: uuid("targetEntityId") .notNull() .references(() => entityTable.id), agentId: uuid("agentId") .notNull() .references(() => agentTable.id), - status: text("status"), - userId: uuid("userId") - .notNull() - .references(() => entityTable.id), + tags: text("tags").array(), + metadata: jsonb("metadata"), }, (table) => [ - index("idx_relationships_users").on(table.userA, table.userB), + index("idx_relationships_users").on(table.sourceEntityId, table.targetEntityId), + unique("unique_relationship").on(table.sourceEntityId, table.targetEntityId, table.agentId), foreignKey({ name: "fk_user_a", - columns: [table.userA], + columns: [table.sourceEntityId], foreignColumns: [entityTable.id], }).onDelete("cascade"), foreignKey({ name: "fk_user_b", - columns: [table.userB], - foreignColumns: [entityTable.id], - }).onDelete("cascade"), - foreignKey({ - name: "fk_user", - columns: [table.userId], + columns: [table.targetEntityId], foreignColumns: [entityTable.id], }).onDelete("cascade"), ] diff --git a/packages/plugin-sql/src/types.ts b/packages/plugin-sql/src/types.ts index acda9cfd8be..69280b0600b 100644 --- a/packages/plugin-sql/src/types.ts +++ b/packages/plugin-sql/src/types.ts @@ -18,7 +18,7 @@ export interface DrizzleOperations { update: (...args: any[]) => any; delete: (...args: any[]) => any; transaction: (cb: (tx: any) => Promise) => Promise; - execute>( + execute<_T = Record>( query: SQL ): Promise<{ rows: any[] } & Record>; } diff --git a/packages/plugin-tee/src/providers/deriveKeyProvider.ts b/packages/plugin-tee/src/providers/deriveKeyProvider.ts index d914b212811..b768ef5b299 100644 --- a/packages/plugin-tee/src/providers/deriveKeyProvider.ts +++ b/packages/plugin-tee/src/providers/deriveKeyProvider.ts @@ -209,11 +209,7 @@ const phalaDeriveKeyProvider: Provider = { * const provider = new MarlinDeriveKeyProvider(); * ``` */ -class MarlinDeriveKeyProvider extends DeriveKeyProvider { - constructor() { - super(); - } -} +class MarlinDeriveKeyProvider extends DeriveKeyProvider {} const marlinDeriveKeyProvider: Provider = { get: async (_runtime: IAgentRuntime, _message?: Memory, _state?: State) => { @@ -228,11 +224,7 @@ const marlinDeriveKeyProvider: Provider = { * const provider = new FleekDeriveKeyProvider(); * ``` */ -class FleekDeriveKeyProvider extends DeriveKeyProvider { - constructor() { - super(); - } -} +class FleekDeriveKeyProvider extends DeriveKeyProvider {} const fleekDeriveKeyProvider: Provider = { get: async (_runtime: IAgentRuntime, _message?: Memory, _state?: State) => { @@ -247,11 +239,7 @@ const fleekDeriveKeyProvider: Provider = { * const provider = new SgxGramineDeriveKeyProvider(); * ``` */ -class SgxGramineDeriveKeyProvider extends DeriveKeyProvider { - constructor() { - super(); - } -} +class SgxGramineDeriveKeyProvider extends DeriveKeyProvider {} const sgxGramineDeriveKeyProvider: Provider = { get: async (_runtime: IAgentRuntime, _message?: Memory, _state?: State) => { diff --git a/packages/plugin-tee/src/providers/remoteAttestationProvider.ts b/packages/plugin-tee/src/providers/remoteAttestationProvider.ts index 5a43fe4b2d4..053282958a3 100644 --- a/packages/plugin-tee/src/providers/remoteAttestationProvider.ts +++ b/packages/plugin-tee/src/providers/remoteAttestationProvider.ts @@ -114,11 +114,7 @@ const phalaRemoteAttestationProvider: Provider = { * const provider = new MarlinRemoteAttestationProvider(); * ``` */ -class MarlinRemoteAttestationProvider extends RemoteAttestationProvider { - constructor() { - super(); - } -} +class MarlinRemoteAttestationProvider extends RemoteAttestationProvider {} const marlinRemoteAttestationProvider: Provider = { get: async (_runtime: IAgentRuntime, _message?: Memory, _state?: State) => { @@ -133,11 +129,7 @@ const marlinRemoteAttestationProvider: Provider = { * const provider = new FleekRemoteAttestationProvider(); * ``` */ -class FleekRemoteAttestationProvider extends RemoteAttestationProvider { - constructor() { - super(); - } -} +class FleekRemoteAttestationProvider extends RemoteAttestationProvider {} const fleekRemoteAttestationProvider: Provider = { get: async (_runtime: IAgentRuntime, _message?: Memory, _state?: State) => { diff --git a/packages/plugin-tee/src/vendors/index.ts b/packages/plugin-tee/src/vendors/index.ts index 9fc75832b4c..5394241fdd6 100644 --- a/packages/plugin-tee/src/vendors/index.ts +++ b/packages/plugin-tee/src/vendors/index.ts @@ -3,7 +3,7 @@ import { PhalaVendor } from './phala'; import { GramineVendor } from './gramine'; import { MarlinVendor } from './marlin'; import { FleekVendor } from './fleek'; -import { TeeVendorNames, TeeVendorName } from './types'; +import { TeeVendorNames, type TeeVendorName } from './types'; const vendors: Record = { [TeeVendorNames.PHALA]: new PhalaVendor(), diff --git a/packages/plugin-telegram/src/messageManager.ts b/packages/plugin-telegram/src/messageManager.ts index 3ef4ab997e0..7f651a477cd 100644 --- a/packages/plugin-telegram/src/messageManager.ts +++ b/packages/plugin-telegram/src/messageManager.ts @@ -1,6 +1,7 @@ import { ChannelType, type Content, + createUniqueUuid, type HandlerCallback, type IAgentRuntime, logger, @@ -8,7 +9,6 @@ import { type Memory, ModelClass, RoleName, - stringToUuid, type UUID } from "@elizaos/core"; import type { Chat, Message, ReactionType, Update } from "@telegraf/types"; @@ -226,15 +226,13 @@ export class MessageManager { try { // Convert IDs to UUIDs - const userId = stringToUuid(ctx.from.id.toString()) as UUID; + const userId = createUniqueUuid(this.runtime, ctx.from.id.toString()) as UUID; const userName = ctx.from.username || ctx.from.first_name || "Unknown User"; - const chatId = stringToUuid(`${ctx.chat?.id.toString()}-${this.runtime.agentId}`) as UUID; + const chatId = createUniqueUuid(this.runtime, ctx.chat?.id.toString()); const roomId = chatId; // Get message ID - const messageId = stringToUuid( - `${roomId}-${message?.message_id?.toString()}` - ) as UUID; + const messageId = createUniqueUuid(this.runtime, message?.message_id?.toString()); // Handle images const imageInfo = await this.processImage(message); @@ -264,7 +262,7 @@ export class MessageManager { userName: userName, // Safely access reply_to_message with type guard inReplyTo: 'reply_to_message' in message && message.reply_to_message ? - stringToUuid(`${message.reply_to_message.message_id.toString()}-${this.runtime.agentId}`) : + createUniqueUuid(this.runtime, message.reply_to_message.message_id.toString()) : undefined }, createdAt: message.date * 1000 @@ -312,10 +310,10 @@ export class MessageManager { // TODO: chat.id is probably used incorrectly here and needs to be fixed const channelType = getChannelType(chat); - const worldId = stringToUuid(`${chat.id.toString()}-${this.runtime.agentId}`) as UUID; + const worldId = createUniqueUuid(this.runtime, chat.id.toString()); const room = {id: roomId, name: roomName, source: "telegram", type: channelType, channelId: ctx.chat.id.toString(), serverId: ctx.chat.id.toString(), worldId: worldId} // TODO: chat.id is probably used incorrectly here and needs to be fixed - const tenantSpecificOwnerId = this.runtime.generateTenantUserId(stringToUuid(chat.id.toString())); + const ownerId = chat.id; // this might be wrong if (channelType === ChannelType.GROUP) { // if the type is a group, we need to get the world id from the supergroup/channel id await this.runtime.ensureWorldExists({ @@ -327,7 +325,7 @@ export class MessageManager { ownership: chat.type === 'supergroup' ? { ownerId: chat.id.toString() } : undefined, roles: { // TODO: chat.id is probably wrong key for this - [tenantSpecificOwnerId]: RoleName.OWNER, + [ownerId]: RoleName.OWNER, }, } }); @@ -349,7 +347,7 @@ export class MessageManager { const isLastMessage = i === sentMessages.length - 1; const responseMemory: Memory = { - id: stringToUuid(`${roomId}-${sentMessage.message_id.toString()}`), + id: createUniqueUuid(this.runtime, sentMessage.message_id.toString()), userId: this.runtime.agentId, agentId: this.runtime.agentId, roomId, @@ -357,7 +355,6 @@ export class MessageManager { ...content, text: sentMessage.text, inReplyTo: messageId, - action: !isLastMessage ? "CONTINUE" : content.action }, createdAt: sentMessage.date * 1000 }; @@ -396,9 +393,10 @@ export class MessageManager { const reactionEmoji = (reaction.new_reaction[0] as ReactionType).type; try { - const userId = stringToUuid(ctx.from.id.toString()); - const roomId = stringToUuid(`${ctx.chat.id.toString()}-${this.runtime.agentId}`); - const reactionId = stringToUuid(`${reaction.message_id}-${ctx.from.id}-${Date.now()}-${this.runtime.agentId}`); + const userId = createUniqueUuid(this.runtime, ctx.from.id.toString()) as UUID; + const roomId = createUniqueUuid(this.runtime, ctx.chat.id.toString()); + + const reactionId = createUniqueUuid(this.runtime, `${reaction.message_id}-${ctx.from.id}-${Date.now()}`); // Create reaction memory const memory: Memory = { @@ -411,7 +409,7 @@ export class MessageManager { source: "telegram", name: ctx.from.first_name, userName: ctx.from.username, - inReplyTo: stringToUuid(`${reaction.message_id.toString()}-${this.runtime.agentId}`) + inReplyTo: createUniqueUuid(this.runtime, reaction.message_id.toString()) }, createdAt: Date.now() }; @@ -422,7 +420,7 @@ export class MessageManager { try { const sentMessage = await ctx.reply(content.text); const responseMemory: Memory = { - id: stringToUuid(`${roomId}-${sentMessage.message_id.toString()}`), + id: createUniqueUuid(this.runtime, sentMessage.message_id.toString()), userId: this.runtime.agentId, agentId: this.runtime.agentId, roomId, diff --git a/packages/plugin-telegram/src/telegramClient.ts b/packages/plugin-telegram/src/telegramClient.ts index b6c89c64a90..68e3737b8b0 100644 --- a/packages/plugin-telegram/src/telegramClient.ts +++ b/packages/plugin-telegram/src/telegramClient.ts @@ -1,6 +1,5 @@ +import { type ClientInstance, type IAgentRuntime, logger } from "@elizaos/core"; import { type Context, Telegraf } from "telegraf"; -import { message } from "telegraf/filters"; -import { type IAgentRuntime, logger, type ClientInstance, stringToUuid, Memory, HandlerCallback, Content } from "@elizaos/core"; import { MessageManager } from "./messageManager.ts"; export class TelegramClient implements ClientInstance { diff --git a/packages/plugin-twitter/src/actions/spaceJoin.ts b/packages/plugin-twitter/src/actions/spaceJoin.ts index 9e24a0f72a9..173491b0a11 100644 --- a/packages/plugin-twitter/src/actions/spaceJoin.ts +++ b/packages/plugin-twitter/src/actions/spaceJoin.ts @@ -5,10 +5,10 @@ import { type Memory, type State, stringToUuid, - HandlerCallback, + type HandlerCallback, logger } from "@elizaos/core"; -import { Tweet } from "../client"; +import type { Tweet } from "../client"; import { SpaceActivity } from "../spaces"; export default { @@ -126,7 +126,7 @@ export default { // If the tweet author isn't hosting a Space, check if any mentioned users are currently hosting one - const agentName = client.state["TWITTER_USERNAME"]; + const agentName = client.state.TWITTER_USERNAME; for (const mention of tweet.mentions) { if (mention.username !== agentName) { const mentionJoined = await joinSpaceByUserName(mention.username); diff --git a/packages/plugin-twitter/src/base.ts b/packages/plugin-twitter/src/base.ts index 275589652cd..82d0b9f1429 100644 --- a/packages/plugin-twitter/src/base.ts +++ b/packages/plugin-twitter/src/base.ts @@ -5,16 +5,16 @@ import { type Memory, type State, type UUID, - logger, - stringToUuid, + createUniqueUuid, + logger } from "@elizaos/core"; +import { EventEmitter } from "node:events"; import { type QueryTweetsResponse, Scraper, SearchMode, type Tweet, } from "./client/index.ts"; -import { EventEmitter } from "node:events"; export function extractAnswer(text: string): string { const startIndex = text.indexOf("Answer: ") + 8; @@ -435,7 +435,7 @@ export class ClientBase extends EventEmitter { const existingMemories = await this.runtime.messageManager.getMemoriesByRoomIds({ roomIds: cachedTimeline.map((tweet) => - stringToUuid(`${tweet.conversationId}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, tweet.conversationId) ), }); @@ -449,7 +449,7 @@ export class ClientBase extends EventEmitter { // Check if any of the cached tweets exist in the existing memories const someCachedTweetsExist = cachedTimeline.some((tweet) => existingMemoryIds.has( - stringToUuid(`${tweet.id}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, tweet.id), ) ); @@ -459,30 +459,24 @@ export class ClientBase extends EventEmitter { (tweet) => tweet.userId !== this.profile.id && !existingMemoryIds.has( - stringToUuid(`${tweet.id}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, tweet.id) ) ); - console.log({ - processingTweets: tweetsToSave.map((tweet) => tweet.id).join(","), - }); - // Save the missing tweets as memories for (const tweet of tweetsToSave) { logger.log("Saving Tweet", tweet.id); - const roomId = stringToUuid( - `${tweet.conversationId}-${this.runtime.agentId}` - ); + const roomId = createUniqueUuid(this.runtime, tweet.conversationId); - const userId = + const userId = createUniqueUuid(this.runtime, tweet.userId === this.profile.id ? this.runtime.agentId - : stringToUuid(tweet.userId); + : tweet.userId); if (tweet.userId === this.profile.id) { continue; - } else { + } await this.runtime.ensureConnection({ userId, roomId, @@ -491,16 +485,13 @@ export class ClientBase extends EventEmitter { source: "twitter", type: ChannelType.FEED }); - } const content = { text: tweet.text, url: tweet.permanentUrl, source: "twitter", inReplyTo: tweet.inReplyToStatusId - ? stringToUuid( - `${tweet.inReplyToStatusId}-${this.runtime.agentId}` - ) + ? createUniqueUuid(this.runtime, tweet.inReplyToStatusId) : undefined, } as Content; @@ -508,7 +499,7 @@ export class ClientBase extends EventEmitter { // check if it already exists const memory = await this.runtime.messageManager.getMemoryById( - stringToUuid(`${tweet.id}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, tweet.id) ); if (memory) { @@ -517,7 +508,7 @@ export class ClientBase extends EventEmitter { } await this.runtime.messageManager.createMemory({ - id: stringToUuid(`${tweet.id}-${this.runtime.agentId}`), + id: createUniqueUuid(this.runtime, tweet.id), userId, content: content, agentId: this.runtime.agentId, @@ -556,7 +547,7 @@ export class ClientBase extends EventEmitter { for (const tweet of allTweets) { tweetIdsToCheck.add(tweet.id); roomIds.add( - stringToUuid(`${tweet.conversationId}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, tweet.conversationId) ); } @@ -576,7 +567,7 @@ export class ClientBase extends EventEmitter { (tweet) => tweet.userId !== this.profile.id && !existingMemoryIds.has( - stringToUuid(`${tweet.id}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, tweet.id) ) ); @@ -586,26 +577,29 @@ export class ClientBase extends EventEmitter { await this.runtime.getOrCreateUser( this.runtime.agentId, - this.profile.username, - this.runtime.character.name, - "twitter" + [this.runtime.character.name], + { + twitter: { + name: this.runtime.character.name, + userName: this.runtime.character.name, + originalUserId: this.runtime.agentId, + }, + } ); - // Save the new tweets as memories for (const tweet of tweetsToSave) { logger.log("Saving Tweet", tweet.id); - const roomId = stringToUuid( - `${tweet.conversationId}-${this.runtime.agentId}` - ); + const roomId = createUniqueUuid(this.runtime, tweet.conversationId); + const userId = tweet.userId === this.profile.id ? this.runtime.agentId - : stringToUuid(tweet.userId); + : createUniqueUuid(this.runtime, tweet.userId); if (tweet.userId === this.profile.id) { continue; - } else { + } await this.runtime.ensureConnection({ userId, roomId, @@ -614,19 +608,18 @@ export class ClientBase extends EventEmitter { source: "twitter", type: ChannelType.FEED }); - } const content = { text: tweet.text, url: tweet.permanentUrl, source: "twitter", inReplyTo: tweet.inReplyToStatusId - ? stringToUuid(tweet.inReplyToStatusId) + ? createUniqueUuid(this.runtime, tweet.inReplyToStatusId) : undefined, } as Content; await this.runtime.messageManager.createMemory({ - id: stringToUuid(`${tweet.id}-${this.runtime.agentId}`), + id: createUniqueUuid(this.runtime, tweet.id), userId, content: content, agentId: this.runtime.agentId, diff --git a/packages/plugin-twitter/src/index.ts b/packages/plugin-twitter/src/index.ts index a192eb8059f..60803562eff 100644 --- a/packages/plugin-twitter/src/index.ts +++ b/packages/plugin-twitter/src/index.ts @@ -1,14 +1,14 @@ -import { logger, stringToUuid, type UUID, type Client, type IAgentRuntime, type Plugin } from "@elizaos/core"; +import { logger, type Client, type IAgentRuntime, type Plugin, type UUID } from "@elizaos/core"; import reply from "./actions/reply.ts"; +import spaceJoin from "./actions/spaceJoin.ts"; import { ClientBase } from "./base.ts"; import { TWITTER_CLIENT_NAME } from "./constants.ts"; import type { TwitterConfig } from "./environment.ts"; import { TwitterInteractionClient } from "./interactions.ts"; import { TwitterPostClient } from "./post.ts"; import { TwitterSpaceClient } from "./spaces.ts"; -import type { ITwitterClient } from "./types.ts"; import { TwitterTestSuite } from "./tests.ts"; -import spaceJoin from "./actions/spaceJoin.ts"; +import type { ITwitterClient } from "./types.ts"; /** * A manager that orchestrates all specialized Twitter logic: @@ -167,7 +167,7 @@ const TwitterClientInterface: Client = { // config.TWITTER_ACCESS_TOKEN && config.TWITTER_ACCESS_TOKEN_SECRET) )) { logger.info("Creating default Twitter client from character settings"); - await manager.createClient(runtime, stringToUuid("default"), config); + await manager.createClient(runtime, runtime.agentId, config); } } catch (error) { logger.error("Failed to create default Twitter client:", error); diff --git a/packages/plugin-twitter/src/interactions.ts b/packages/plugin-twitter/src/interactions.ts index fed0d4b5912..cb84e666bb7 100644 --- a/packages/plugin-twitter/src/interactions.ts +++ b/packages/plugin-twitter/src/interactions.ts @@ -1,21 +1,21 @@ -import { SearchMode, type Tweet } from "./client/index.ts"; import { + ChannelType, composeContext, + type Content, + createUniqueUuid, generateMessageResponse, generateShouldRespond, - messageCompletionFooter, - shouldRespondFooter, - type Content, type HandlerCallback, type IAgentRuntime, + logger, type Memory, + messageCompletionFooter, ModelClass, - type State, - stringToUuid, - logger, - ChannelType, + shouldRespondFooter, + type State } from "@elizaos/core"; import type { ClientBase } from "./base.ts"; +import { SearchMode, type Tweet } from "./client/index.ts"; import { buildConversationThread, sendTweet, wait } from "./utils.ts"; export const twitterMessageHandlerTemplate = @@ -236,9 +236,7 @@ export class TwitterInteractionClient { BigInt(tweet.id) > this.client.lastCheckedTweetId ) { // Generate the tweetId UUID the same way it's done in handleTweet - const tweetId = stringToUuid( - `${tweet.id}-${this.runtime.agentId}` - ); + const tweetId = createUniqueUuid(this.runtime, tweet.id); // Check if we've already processed this tweet const existingResponse = @@ -254,14 +252,14 @@ export class TwitterInteractionClient { } logger.log("New Tweet found", tweet.permanentUrl); - const roomId = stringToUuid( - `${tweet.conversationId}-${this.runtime.agentId}` - ); + const roomId = createUniqueUuid(this.runtime, tweet.conversationId); - const userIdUUID = + const userIdUUID = createUniqueUuid( + this.runtime, tweet.userId === this.client.profile.id ? this.runtime.agentId - : stringToUuid(tweet.userId!); + : tweet.userId + ); await this.runtime.ensureConnection({ userId: userIdUUID, @@ -373,17 +371,15 @@ export class TwitterInteractionClient { }); // check if the tweet exists, save if it doesn't - const tweetId = stringToUuid(`${tweet.id}-${this.runtime.agentId}`); + const tweetId = createUniqueUuid(this.runtime, tweet.id); const tweetExists = await this.runtime.messageManager.getMemoryById(tweetId); if (!tweetExists) { logger.log("tweet does not exist, saving"); - const userIdUUID = stringToUuid(`${tweet.userId}-${this.runtime.agentId}`); + const userIdUUID = createUniqueUuid(this.runtime, tweet.userId); - const roomId = stringToUuid( - `${tweet.conversationId}-${this.runtime.agentId}` - ); + const roomId = createUniqueUuid(this.runtime, tweet.conversationId); await this.runtime.ensureConnection({ userId: userIdUUID, @@ -402,9 +398,7 @@ export class TwitterInteractionClient { url: tweet.permanentUrl, imageUrls: tweet.photos?.map(photo => photo.url) || [], inReplyTo: tweet.inReplyToStatusId - ? stringToUuid( - `${tweet.inReplyToStatusId}-${this.runtime.agentId}` - ) + ? createUniqueUuid(this.runtime, tweet.inReplyToStatusId) : undefined, }, userId: userIdUUID, @@ -479,9 +473,9 @@ export class TwitterInteractionClient { const removeQuotes = (str: string) => str.replace(/^['"](.*)['"]$/, "$1"); - const stringId = stringToUuid(`${tweet.id}-${this.runtime.agentId}`); + const replyToId = createUniqueUuid(this.runtime, tweet.id); - response.inReplyTo = stringId; + response.inReplyTo = replyToId; response.text = removeQuotes(response.text); @@ -507,7 +501,7 @@ export class TwitterInteractionClient { }; const responseMessages = [{ - id: stringToUuid(`${tweet.id}-${this.runtime.agentId}`), + id: createUniqueUuid(this.runtime, tweet.id), userId: this.runtime.agentId, agentId: this.runtime.agentId, content: response, @@ -520,14 +514,6 @@ export class TwitterInteractionClient { )) as State; for (const responseMessage of responseMessages) { - if ( - responseMessage === - responseMessages[responseMessages.length - 1] - ) { - responseMessage.content.action = response.action; - } else { - responseMessage.content.action = "CONTINUE"; - } await this.runtime.messageManager.createMemory( responseMessage ); @@ -586,13 +572,11 @@ export class TwitterInteractionClient { // Handle memory storage const memory = await this.runtime.messageManager.getMemoryById( - stringToUuid(`${currentTweet.id}-${this.runtime.agentId}`) + createUniqueUuid(this.runtime, currentTweet.id) ); if (!memory) { - const roomId = stringToUuid( - `${currentTweet.conversationId}-${this.runtime.agentId}` - ); - const userId = stringToUuid(currentTweet.userId); + const roomId = createUniqueUuid(this.runtime, tweet.conversationId); + const userId = createUniqueUuid(this.runtime, currentTweet.userId); await this.runtime.ensureConnection({ userId, @@ -604,9 +588,7 @@ export class TwitterInteractionClient { }); this.runtime.messageManager.createMemory({ - id: stringToUuid( - `${currentTweet.id}-${this.runtime.agentId}` - ), + id: createUniqueUuid(this.runtime, currentTweet.id), agentId: this.runtime.agentId, content: { text: currentTweet.text, @@ -614,9 +596,7 @@ export class TwitterInteractionClient { url: currentTweet.permanentUrl, imageUrls: currentTweet.photos?.map(photo => photo.url) || [], inReplyTo: currentTweet.inReplyToStatusId - ? stringToUuid( - `${currentTweet.inReplyToStatusId}-${this.runtime.agentId}` - ) + ? createUniqueUuid(this.runtime, currentTweet.inReplyToStatusId) : undefined, }, createdAt: currentTweet.timestamp * 1000, @@ -624,7 +604,7 @@ export class TwitterInteractionClient { userId: currentTweet.userId === this.twitterUserId ? this.runtime.agentId - : stringToUuid(currentTweet.userId), + : createUniqueUuid(this.runtime, currentTweet.userId), }); } diff --git a/packages/plugin-twitter/src/post.ts b/packages/plugin-twitter/src/post.ts index ccb4d0dfced..5c7a1c86592 100644 --- a/packages/plugin-twitter/src/post.ts +++ b/packages/plugin-twitter/src/post.ts @@ -2,21 +2,21 @@ import { ChannelType, cleanJsonResponse, composeContext, + createUniqueUuid, extractAttributes, generateText, type IAgentRuntime, logger, ModelClass, parseJSONObjectFromText, - stringToUuid, truncateToCompleteSentence, type UUID } from "@elizaos/core"; import type { ClientBase } from "./base.ts"; import type { Tweet } from "./client/index.ts"; +import { twitterPostTemplate } from "./templates.ts"; import type { MediaData } from "./types.ts"; import { fetchMediaData } from "./utils.ts"; -import { twitterPostTemplate } from "./templates.ts"; export class TwitterPostClient { @@ -162,7 +162,7 @@ export class TwitterPostClient { // Create a memory for the tweet await runtime.messageManager.createMemory({ - id: stringToUuid(`${tweet.id}-${runtime.agentId}`), + id: createUniqueUuid(this.runtime, tweet.id), userId: runtime.agentId, agentId: runtime.agentId, content: { @@ -292,14 +292,17 @@ export class TwitterPostClient { logger.log("Generating new tweet"); try { - const roomId = stringToUuid( - `twitter_generate_room-${this.client.profile.username}` - ); + const roomId = createUniqueUuid(this.runtime, 'twitter_generate'); await this.runtime.getOrCreateUser( this.runtime.agentId, - this.client.profile.username, - this.runtime.character.name, - "twitter" + [this.client.profile.username], + { + twitter: { + name: this.client.profile.username, + userName: this.client.profile.username, + originalUserId: this.runtime.agentId, + }, + } ); const topics = this.runtime.character.topics diff --git a/packages/plugin-twitter/src/sttTtsSpaces.ts b/packages/plugin-twitter/src/sttTtsSpaces.ts index 12550309e2d..50dbb6e79f1 100644 --- a/packages/plugin-twitter/src/sttTtsSpaces.ts +++ b/packages/plugin-twitter/src/sttTtsSpaces.ts @@ -1,24 +1,24 @@ // src/plugins/SttTtsPlugin.ts import { + ChannelType, type Content, + type HandlerCallback, type IAgentRuntime, type Memory, - type Plugin, - logger, ModelClass, - stringToUuid, - ChannelType, - HandlerCallback + type Plugin, + createUniqueUuid, + logger } from "@elizaos/core"; +import { spawn } from "node:child_process"; +import type { Readable } from "node:stream"; +import type { ClientBase } from "./base"; import type { AudioDataWithUser, JanusClient, Space, } from "./client"; -import { spawn } from "node:child_process"; -import type { ClientBase } from "./base"; -import { Readable } from "node:stream"; interface PluginConfig { runtime: IAgentRuntime; @@ -345,17 +345,21 @@ export class SttTtsPlugin implements Plugin { // Extract the numeric ID part const numericId = userId.replace("tw-", ""); - const roomId = stringToUuid(`twitter_generate_room-${this.spaceId}`); + const roomId = createUniqueUuid(this.runtime, `twitter_generate_room-${this.spaceId}`); // Create consistent UUID for the user - const userUuid = stringToUuid(`twitter-user-${numericId}`); + const userUuid = createUniqueUuid(this.runtime, numericId); // Ensure the user exists in the accounts table await this.runtime.getOrCreateUser( userUuid, - userId, // Use full Twitter ID as username - `Twitter User ${numericId}`, - "twitter", + [userId], + { + twitter: { + name: userId, + userName: userId, + }, + }, ); // Ensure room exists and user is in it @@ -363,7 +367,7 @@ export class SttTtsPlugin implements Plugin { await this.runtime.ensureParticipantInRoom(userUuid, roomId); const memory = { - id: stringToUuid(`${roomId}-voice-message-${Date.now()}`), + id: createUniqueUuid(this.runtime, `${roomId}-voice-message-${Date.now()}`), agentId: this.runtime.agentId, content: { text: userText, @@ -377,7 +381,7 @@ export class SttTtsPlugin implements Plugin { const callback: HandlerCallback = async (content: Content, _files: any[] = []) => { try { const responseMemory: Memory = { - id: stringToUuid(`${memory.id}-voice-response-${Date.now()}`), + id: createUniqueUuid(this.runtime, `${memory.id}-voice-response-${Date.now()}`), userId: this.runtime.agentId, agentId: this.runtime.agentId, content: { diff --git a/packages/plugin-twitter/src/tests.ts b/packages/plugin-twitter/src/tests.ts index 97db96a11b9..ada2c3ba74b 100644 --- a/packages/plugin-twitter/src/tests.ts +++ b/packages/plugin-twitter/src/tests.ts @@ -4,6 +4,7 @@ import { type IAgentRuntime, ModelClass, stringToUuid, + createUniqueUuid, } from "@elizaos/core"; import type { TwitterClient } from "./index.ts"; import { SearchMode } from "./client/index.ts"; @@ -131,9 +132,8 @@ export class TwitterTestSuite implements TestSuite { async testPostTweet(runtime: IAgentRuntime) { try { - const roomId = stringToUuid( - `twitter_mock_room-${this.twitterClient.client.profile.username}` - ); + const roomId = createUniqueUuid(runtime, 'twitter_mock_room'); + const postClient = this.twitterClient.post; const tweetText = await this.generateRandomTweetContent(runtime); await postClient.postTweet( @@ -152,9 +152,8 @@ export class TwitterTestSuite implements TestSuite { async testPostImageTweet(runtime: IAgentRuntime) { try { - const roomId = stringToUuid( - `twitter_mock_room-${this.twitterClient.client.profile.username}` - ); + const roomId = createUniqueUuid(runtime, 'twitter_mock_room'); + const postClient = this.twitterClient.post; const tweetText = await this.generateRandomTweetContent( runtime, @@ -209,8 +208,8 @@ export class TwitterTestSuite implements TestSuite { message: { content: { text: testTweet.text, source: "twitter" }, agentId: runtime.agentId, - userId: stringToUuid(testTweet.userId), - roomId: stringToUuid(testTweet.conversationId), + userId: createUniqueUuid(runtime, testTweet.userId), + roomId: createUniqueUuid(runtime, testTweet.conversationId), }, thread: [], }); diff --git a/packages/plugin-twitter/src/utils.ts b/packages/plugin-twitter/src/utils.ts index 6d257d5e837..d0dc4ab552f 100644 --- a/packages/plugin-twitter/src/utils.ts +++ b/packages/plugin-twitter/src/utils.ts @@ -1,13 +1,11 @@ -import type { Tweet } from "./client"; -import { Content, IAgentRuntime, Memory, ModelClass, UUID, composeContext } from "@elizaos/core"; -import { ChannelType, generateText, stringToUuid } from "@elizaos/core"; -import type { ClientBase } from "./base"; -import { logger } from "@elizaos/core"; import type { Media, State } from "@elizaos/core"; +import { ChannelType, type Content, type IAgentRuntime, type Memory, ModelClass, type UUID, composeContext, createUniqueUuid, generateText, logger } from "@elizaos/core"; import fs from "node:fs"; import path from "node:path"; +import type { ClientBase } from "./base"; +import type { Tweet } from "./client"; +import type { SttTtsPlugin } from "./sttTtsSpaces"; import type { ActionResponse, MediaData } from "./types"; -import { SttTtsPlugin } from "./sttTtsSpaces"; export const wait = (minTime = 1000, maxTime = 3000) => { const waitTime = @@ -58,13 +56,11 @@ export async function buildConversationThread( // Handle memory storage const memory = await client.runtime.messageManager.getMemoryById( - stringToUuid(`${currentTweet.id}-${client.runtime.agentId}`) + createUniqueUuid(this.runtime, currentTweet.id) ); if (!memory) { - const roomId = stringToUuid( - `${currentTweet.conversationId}-${client.runtime.agentId}` - ); - const userId = stringToUuid(currentTweet.userId); + const roomId = createUniqueUuid(this.runtime, currentTweet.conversationId); + const userId = createUniqueUuid(this.runtime, currentTweet.userId); await client.runtime.ensureConnection({ userId, @@ -76,9 +72,7 @@ export async function buildConversationThread( }); await client.runtime.messageManager.createMemory({ - id: stringToUuid( - `${currentTweet.id}-${client.runtime.agentId}` - ), + id: createUniqueUuid(this.runtime, currentTweet.id), agentId: client.runtime.agentId, content: { text: currentTweet.text, @@ -86,9 +80,7 @@ export async function buildConversationThread( url: currentTweet.permanentUrl, imageUrls: currentTweet.photos.map((p) => p.url) || [], inReplyTo: currentTweet.inReplyToStatusId - ? stringToUuid( - `${currentTweet.inReplyToStatusId}-${client.runtime.agentId}` - ) + ? createUniqueUuid(this.runtime, currentTweet.inReplyToStatusId) : undefined, }, createdAt: currentTweet.timestamp * 1000, @@ -96,7 +88,7 @@ export async function buildConversationThread( userId: currentTweet.userId === client.profile.id ? client.runtime.agentId - : stringToUuid(currentTweet.userId), + : createUniqueUuid(this.runtime, currentTweet.userId), }); } @@ -267,7 +259,7 @@ export async function sendTweet( } const memories: Memory[] = sentTweets.map((tweet) => ({ - id: stringToUuid(`${tweet.id}-${client.runtime.agentId}`), + id: createUniqueUuid(client.runtime, tweet.id), agentId: client.runtime.agentId, userId: client.runtime.agentId, content: { @@ -277,9 +269,7 @@ export async function sendTweet( url: tweet.permanentUrl, imageUrls: tweet.photos.map((p) => p.url) || [], inReplyTo: tweet.inReplyToStatusId - ? stringToUuid( - `${tweet.inReplyToStatusId}-${client.runtime.agentId}` - ) + ? createUniqueUuid(client.runtime, tweet.inReplyToStatusId) : undefined, }, roomId, @@ -627,7 +617,7 @@ Example: export async function isAgentInSpace(client: ClientBase, spaceId: string): Promise { const space = await client.twitterClient.getAudioSpaceById(spaceId); - const agentName = client.state["TWITTER_USERNAME"]; + const agentName = client.state.TWITTER_USERNAME; return space.participants.listeners.some( (participant) => participant.twitter_screen_name === agentName