Repository: bytebot-ai/bytebot Branch: main Commit: 3d37894ce07e Files: 316 Total size: 857.6 KB Directory structure: gitextract_hhcv8dmu/ ├── .github/ │ └── workflows/ │ ├── build-agent.yaml │ ├── build-desktop.yaml │ └── build-ui.yaml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── docker/ │ ├── bytebot-desktop.Dockerfile │ ├── docker-compose-claude-code.yml │ ├── docker-compose.core.yml │ ├── docker-compose.development.yml │ ├── docker-compose.proxy.yml │ └── docker-compose.yml ├── docs/ │ ├── api-reference/ │ │ ├── agent/ │ │ │ ├── tasks.mdx │ │ │ └── ui.mdx │ │ ├── computer-use/ │ │ │ ├── examples.mdx │ │ │ ├── openapi.json │ │ │ └── unified-endpoint.mdx │ │ ├── endpoint/ │ │ │ ├── create.mdx │ │ │ ├── delete.mdx │ │ │ ├── get.mdx │ │ │ └── webhook.mdx │ │ ├── introduction.mdx │ │ └── openapi.json │ ├── core-concepts/ │ │ ├── agent-system.mdx │ │ ├── architecture.mdx │ │ ├── desktop-environment.mdx │ │ └── rpa-comparison.mdx │ ├── deployment/ │ │ ├── helm.mdx │ │ ├── litellm.mdx │ │ └── railway.mdx │ ├── docs.json │ ├── guides/ │ │ ├── password-management.mdx │ │ ├── takeover-mode.mdx │ │ └── task-creation.mdx │ ├── introduction.mdx │ ├── quickstart.mdx │ └── rest-api/ │ ├── computer-use.mdx │ ├── examples.mdx │ ├── input-tracking.mdx │ └── introduction.mdx ├── helm/ │ ├── Chart.yaml │ ├── README.md │ ├── charts/ │ │ ├── bytebot-agent/ │ │ │ ├── Chart.yaml │ │ │ ├── templates/ │ │ │ │ ├── _helpers.tpl │ │ │ │ ├── deployment.yaml │ │ │ │ ├── ingress.yaml │ │ │ │ ├── secret.yaml │ │ │ │ └── service.yaml │ │ │ └── values.yaml │ │ ├── bytebot-desktop/ │ │ │ ├── Chart.yaml │ │ │ ├── templates/ │ │ │ │ ├── _helpers.tpl │ │ │ │ ├── deployment.yaml │ │ │ │ ├── ingress.yaml │ │ │ │ ├── pvc.yaml │ │ │ │ └── service.yaml │ │ │ └── values.yaml │ │ ├── bytebot-llm-proxy/ │ │ │ ├── Chart.yaml │ │ │ ├── templates/ │ │ │ │ ├── _helpers.tpl │ │ │ │ ├── configmap.yaml │ │ │ │ ├── deployment.yaml │ │ │ │ ├── ingress.yaml │ │ │ │ ├── secret.yaml │ │ │ │ └── service.yaml │ │ │ └── values.yaml │ │ ├── bytebot-ui/ │ │ │ ├── Chart.yaml │ │ │ ├── templates/ │ │ │ │ ├── _helpers.tpl │ │ │ │ ├── deployment.yaml │ │ │ │ ├── hpa.yaml │ │ │ │ ├── ingress.yaml │ │ │ │ └── service.yaml │ │ │ └── values.yaml │ │ └── postgresql/ │ │ ├── Chart.yaml │ │ ├── templates/ │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── secret.yaml │ │ │ └── service.yaml │ │ └── values.yaml │ ├── templates/ │ │ ├── NOTES.txt │ │ └── ingress.yaml │ ├── values-proxy.yaml │ ├── values-simple.yaml │ └── values.yaml └── packages/ ├── bytebot-agent/ │ ├── .dockerignore │ ├── .gitignore │ ├── .prettierrc │ ├── Dockerfile │ ├── eslint.config.mjs │ ├── nest-cli.json │ ├── package.json │ ├── prisma/ │ │ ├── migrations/ │ │ │ ├── 20250328022708_initial_migration/ │ │ │ │ └── migration.sql │ │ │ ├── 20250413053912_message_role/ │ │ │ │ └── migration.sql │ │ │ ├── 20250522200556_updated_task_structure/ │ │ │ │ └── migration.sql │ │ │ ├── 20250523162632_add_scheduling/ │ │ │ │ └── migration.sql │ │ │ ├── 20250529003255_tasks_control/ │ │ │ │ └── migration.sql │ │ │ ├── 20250530012753_tasks_control/ │ │ │ │ └── migration.sql │ │ │ ├── 20250619013027_add_better_auth_schema/ │ │ │ │ └── migration.sql │ │ │ ├── 20250622195148_add_user_to_task/ │ │ │ │ └── migration.sql │ │ │ ├── 20250706223912_model_picker/ │ │ │ │ └── migration.sql │ │ │ ├── 20250722041608_files/ │ │ │ │ └── migration.sql │ │ │ ├── 20250820172813_remove_auth/ │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── src/ │ │ ├── agent/ │ │ │ ├── agent.analytics.ts │ │ │ ├── agent.computer-use.ts │ │ │ ├── agent.constants.ts │ │ │ ├── agent.module.ts │ │ │ ├── agent.processor.ts │ │ │ ├── agent.scheduler.ts │ │ │ ├── agent.tools.ts │ │ │ ├── agent.types.ts │ │ │ └── input-capture.service.ts │ │ ├── anthropic/ │ │ │ ├── anthropic.constants.ts │ │ │ ├── anthropic.module.ts │ │ │ ├── anthropic.service.ts │ │ │ └── anthropic.tools.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── google/ │ │ │ ├── google.constants.ts │ │ │ ├── google.module.ts │ │ │ ├── google.service.ts │ │ │ └── google.tools.ts │ │ ├── main.ts │ │ ├── messages/ │ │ │ ├── messages.module.ts │ │ │ └── messages.service.ts │ │ ├── openai/ │ │ │ ├── openai.constants.ts │ │ │ ├── openai.module.ts │ │ │ ├── openai.service.ts │ │ │ └── openai.tools.ts │ │ ├── prisma/ │ │ │ ├── prisma.module.ts │ │ │ └── prisma.service.ts │ │ ├── proxy/ │ │ │ ├── proxy.module.ts │ │ │ ├── proxy.service.ts │ │ │ └── proxy.tools.ts │ │ ├── summaries/ │ │ │ ├── summaries.modue.ts │ │ │ └── summaries.service.ts │ │ └── tasks/ │ │ ├── dto/ │ │ │ ├── add-task-message.dto.ts │ │ │ ├── create-task.dto.ts │ │ │ └── update-task.dto.ts │ │ ├── tasks.controller.ts │ │ ├── tasks.gateway.ts │ │ ├── tasks.module.ts │ │ └── tasks.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── bytebot-agent-cc/ │ ├── .dockerignore │ ├── .gitignore │ ├── .prettierrc │ ├── Dockerfile │ ├── eslint.config.mjs │ ├── nest-cli.json │ ├── package.json │ ├── prisma/ │ │ ├── migrations/ │ │ │ ├── 20250328022708_initial_migration/ │ │ │ │ └── migration.sql │ │ │ ├── 20250413053912_message_role/ │ │ │ │ └── migration.sql │ │ │ ├── 20250522200556_updated_task_structure/ │ │ │ │ └── migration.sql │ │ │ ├── 20250523162632_add_scheduling/ │ │ │ │ └── migration.sql │ │ │ ├── 20250529003255_tasks_control/ │ │ │ │ └── migration.sql │ │ │ ├── 20250530012753_tasks_control/ │ │ │ │ └── migration.sql │ │ │ ├── 20250619013027_add_better_auth_schema/ │ │ │ │ └── migration.sql │ │ │ ├── 20250622195148_add_user_to_task/ │ │ │ │ └── migration.sql │ │ │ ├── 20250706223912_model_picker/ │ │ │ │ └── migration.sql │ │ │ ├── 20250722041608_files/ │ │ │ │ └── migration.sql │ │ │ ├── 20250820172813_remove_auth/ │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── src/ │ │ ├── agent/ │ │ │ ├── agent.analytics.ts │ │ │ ├── agent.computer-use.ts │ │ │ ├── agent.constants.ts │ │ │ ├── agent.module.ts │ │ │ ├── agent.processor.ts │ │ │ ├── agent.scheduler.ts │ │ │ ├── agent.tools.ts │ │ │ ├── agent.types.ts │ │ │ └── input-capture.service.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── main.ts │ │ ├── messages/ │ │ │ ├── messages.module.ts │ │ │ └── messages.service.ts │ │ ├── prisma/ │ │ │ ├── prisma.module.ts │ │ │ └── prisma.service.ts │ │ └── tasks/ │ │ ├── dto/ │ │ │ ├── add-task-message.dto.ts │ │ │ ├── create-task.dto.ts │ │ │ └── update-task.dto.ts │ │ ├── tasks.controller.ts │ │ ├── tasks.gateway.ts │ │ ├── tasks.module.ts │ │ └── tasks.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── bytebot-llm-proxy/ │ ├── Dockerfile │ └── litellm-config.yaml ├── bytebot-ui/ │ ├── .dockerignore │ ├── .gitignore │ ├── .prettierrc.json │ ├── Dockerfile │ ├── components.json │ ├── eslint.config.mjs │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── server.ts │ ├── src/ │ │ ├── app/ │ │ │ ├── api/ │ │ │ │ └── [[...path]]/ │ │ │ │ └── route.ts │ │ │ ├── desktop/ │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── tasks/ │ │ │ ├── [id]/ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── VirtualDesktopStatusHeader.tsx │ │ │ ├── layout/ │ │ │ │ └── Header.tsx │ │ │ ├── messages/ │ │ │ │ ├── AssistantMessage.tsx │ │ │ │ ├── ChatContainer.tsx │ │ │ │ ├── ChatInput.tsx │ │ │ │ ├── MessageAvatar.tsx │ │ │ │ ├── MessageGroup.tsx │ │ │ │ ├── UserMessage.tsx │ │ │ │ └── content/ │ │ │ │ ├── ComputerToolContent.tsx │ │ │ │ ├── ComputerToolContentNormal.tsx │ │ │ │ ├── ComputerToolContentTakeOver.tsx │ │ │ │ ├── ComputerToolUtils.tsx │ │ │ │ ├── ErrorContent.tsx │ │ │ │ ├── ImageContent.tsx │ │ │ │ ├── MessageContent.tsx │ │ │ │ └── TextContent.tsx │ │ │ ├── screenshot/ │ │ │ │ └── ScreenshotViewer.tsx │ │ │ ├── tasks/ │ │ │ │ ├── TaskItem.tsx │ │ │ │ ├── TaskList.tsx │ │ │ │ └── TaskTabs.tsx │ │ │ ├── ui/ │ │ │ │ ├── TopicPopover.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── copy-button.tsx │ │ │ │ ├── desktop-container.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── loader.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── switch.tsx │ │ │ │ └── text-shimmer.tsx │ │ │ └── vnc/ │ │ │ └── VncViewer.tsx │ │ ├── constants/ │ │ │ └── ui.constants.ts │ │ ├── hooks/ │ │ │ ├── useChatSession.ts │ │ │ ├── useScrollScreenshot.ts │ │ │ └── useWebSocket.ts │ │ ├── lib/ │ │ │ └── utils.ts │ │ ├── types/ │ │ │ └── index.ts │ │ └── utils/ │ │ ├── clipboard.ts │ │ ├── screenshotUtils.ts │ │ ├── stringUtils.ts │ │ └── taskUtils.ts │ └── tsconfig.json ├── bytebotd/ │ ├── .dockerignore │ ├── .prettierrc │ ├── Dockerfile │ ├── eslint.config.mjs │ ├── nest-cli.json │ ├── package.json │ ├── root/ │ │ ├── etc/ │ │ │ ├── firefox/ │ │ │ │ └── policies/ │ │ │ │ └── policies.json │ │ │ ├── lightdm/ │ │ │ │ └── lightdm.conf.d/ │ │ │ │ └── 50-autologin.conf │ │ │ ├── supervisor/ │ │ │ │ └── conf.d/ │ │ │ │ └── supervisord.conf │ │ │ └── thunderbird/ │ │ │ └── policies/ │ │ │ └── policies.json │ │ ├── home/ │ │ │ └── user/ │ │ │ ├── .config/ │ │ │ │ └── xfce4/ │ │ │ │ ├── desktop/ │ │ │ │ │ └── icons.screen0-1264x913.rc │ │ │ │ ├── helpers.rc │ │ │ │ ├── terminal/ │ │ │ │ │ └── accels.scm │ │ │ │ └── xfconf/ │ │ │ │ └── xfce-perchannel-xml/ │ │ │ │ ├── displays.xml │ │ │ │ ├── thunar.xml │ │ │ │ ├── xfce4-appfinder.xml │ │ │ │ ├── xfce4-desktop.xml │ │ │ │ ├── xfce4-keyboard-shortcuts.xml │ │ │ │ ├── xfce4-notifyd.xml │ │ │ │ ├── xfce4-panel.xml │ │ │ │ └── xfwm4.xml │ │ │ └── .xsessionrc │ │ └── usr/ │ │ └── share/ │ │ └── applications/ │ │ ├── 1password.desktop │ │ ├── code.desktop │ │ ├── firefox.desktop │ │ ├── terminal.desktop │ │ └── thunderbird.desktop │ ├── src/ │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── computer-use/ │ │ │ ├── computer-use.controller.ts │ │ │ ├── computer-use.module.ts │ │ │ ├── computer-use.service.ts │ │ │ └── dto/ │ │ │ ├── base.dto.ts │ │ │ ├── computer-action-validation.pipe.ts │ │ │ └── computer-action.dto.ts │ │ ├── input-tracking/ │ │ │ ├── input-tracking.controller.ts │ │ │ ├── input-tracking.gateway.ts │ │ │ ├── input-tracking.helpers.ts │ │ │ ├── input-tracking.module.ts │ │ │ └── input-tracking.service.ts │ │ ├── main.ts │ │ ├── mcp/ │ │ │ ├── bytebot-mcp.module.ts │ │ │ ├── compressor.ts │ │ │ ├── computer-use.tools.ts │ │ │ └── index.ts │ │ └── nut/ │ │ ├── nut.module.ts │ │ └── nut.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── shared/ ├── package.json ├── src/ │ ├── index.ts │ ├── types/ │ │ ├── computerAction.types.ts │ │ └── messageContent.types.ts │ └── utils/ │ ├── computerAction.utils.ts │ └── messageContent.utils.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build-agent.yaml ================================================ name: Build Agent on: push: branches: - main paths: - "packages/bytebot-agent/**" - "packages/shared/**" permissions: contents: read packages: write jobs: docker: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/bytebot-ai/bytebot-agent tags: type=edge - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 env: BUILDX_NO_DEFAULT_ATTESTATIONS: 1 DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false with: context: ./packages file: ./packages/bytebot-agent/Dockerfile platforms: linux/amd64,linux/arm64 push: true cache-from: type=gha cache-to: type=gha,mode=max tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/build-desktop.yaml ================================================ name: Build Desktop on: push: branches: - main paths: - "docker/**" - "packages/bytebotd/**" permissions: contents: read packages: write jobs: docker: runs-on: ubuntu-22.04 steps: # 1. Check out code - uses: actions/checkout@v4 # 2. Enable QEMU so the amd64 runner can cross‑build arm64 - uses: docker/setup-qemu-action@v3 # 3. Set up Buildx builder - uses: docker/setup-buildx-action@v3 # 4. Generate OCI labels + the single "edge" tag - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/bytebot-ai/bytebot-desktop tags: type=edge # 5. Log in to GHCR - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # 6. Build & push a multi‑arch image - name: Build and push uses: docker/build-push-action@v6 env: BUILDX_NO_DEFAULT_ATTESTATIONS: 1 # hide "unknown/unknown" in GHCR DOCKER_BUILD_SUMMARY: false # keep logs concise DOCKER_BUILD_RECORD_UPLOAD: false with: context: ./packages/ file: ./packages/bytebotd/Dockerfile platforms: linux/amd64,linux/arm64 # build both archs in one go push: true cache-from: type=gha cache-to: type=gha,mode=max tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/build-ui.yaml ================================================ name: Build UI on: push: branches: - main paths: - "packages/bytebot-ui/**" - "packages/shared/**" permissions: contents: read packages: write jobs: docker: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/bytebot-ai/bytebot-ui tags: type=edge - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 env: BUILDX_NO_DEFAULT_ATTESTATIONS: 1 DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false with: context: ./packages file: ./packages/bytebot-ui/Dockerfile platforms: linux/amd64,linux/arm64 push: true cache-from: type=gha cache-to: type=gha,mode=max tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* *.qcow2 *.iso *.img *.vdi *.vmdk *.vhdx *.vhd # compiled output agent/dist agent/node_modules agent/build # Logs logs *.log npm-debug.log* pnpm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # OS .DS_Store # Tests agent/coverage agent/.nyc_output # IDEs and editors agent/.idea agent/.project agent/.classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # dotenv environment variable files .env.development.local .env.test.local .env.production.local .env.local # temp directory .temp .tmp # Runtime data pids *.pid *.seed *.pid.lock # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # QEMU *.qcow2 ================================================ FILE: .prettierignore ================================================ # Ignore formatting in docs folder /docs/** ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================
Bytebot Logo # Bytebot: Open-Source AI Desktop Agent bytebot-ai%2Fbytebot | Trendshift **An AI that has its own computer to complete tasks for you** [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ) [![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://github.com/bytebot-ai/bytebot/tree/main/docker) [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE) [![Discord](https://img.shields.io/discord/1232768900274585720?color=7289da&label=discord)](https://discord.com/invite/d9ewZkWPTP) [🌐 Website](https://bytebot.ai) • [📚 Documentation](https://docs.bytebot.ai) • [💬 Discord](https://discord.com/invite/d9ewZkWPTP) • [𝕏 Twitter](https://x.com/bytebot_ai) [Deutsch](https://zdoc.app/de/bytebot-ai/bytebot) | [Español](https://zdoc.app/es/bytebot-ai/bytebot) | [français](https://zdoc.app/fr/bytebot-ai/bytebot) | [日本語](https://zdoc.app/ja/bytebot-ai/bytebot) | [한국어](https://zdoc.app/ko/bytebot-ai/bytebot) | [Português](https://zdoc.app/pt/bytebot-ai/bytebot) | [Русский](https://zdoc.app/ru/bytebot-ai/bytebot) | [中文](https://zdoc.app/zh/bytebot-ai/bytebot)
--- https://github.com/user-attachments/assets/f271282a-27a3-43f3-9b99-b34007fdd169 https://github.com/user-attachments/assets/72a43cf2-bd87-44c5-a582-e7cbe176f37f ## What is a Desktop Agent? A desktop agent is an AI that has its own computer. Unlike browser-only agents or traditional RPA tools, Bytebot comes with a full virtual desktop where it can: - Use any application (browsers, email clients, office tools, IDEs) - Download and organize files with its own file system - Log into websites and applications using password managers - Read and process documents, PDFs, and spreadsheets - Complete complex multi-step workflows across different programs Think of it as a virtual employee with their own computer who can see the screen, move the mouse, type on the keyboard, and complete tasks just like a human would. ## Why Give AI Its Own Computer? When AI has access to a complete desktop environment, it unlocks capabilities that aren't possible with browser-only agents or API integrations: ### Complete Task Autonomy Give Bytebot a task like "Download all invoices from our vendor portals and organize them into a folder" and it will: - Open the browser - Navigate to each portal - Handle authentication (including 2FA via password managers) - Download the files to its local file system - Organize them into a folder ### Process Documents Upload files directly to Bytebot's desktop and it can: - Read entire PDFs into its context - Extract data from complex documents - Cross-reference information across multiple files - Create new documents based on analysis - Handle formats that APIs can't access ### Use Real Applications Bytebot isn't limited to web interfaces. It can: - Use desktop applications like text editors, VS Code, or email clients - Run scripts and command-line tools - Install new software as needed - Configure applications for specific workflows ## Quick Start ### Deploy in 2 Minutes **Option 1: Railway (Easiest)** [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ) Just click and add your AI provider API key. **Option 2: Docker Compose** ```bash git clone https://github.com/bytebot-ai/bytebot.git cd bytebot # Add your AI provider key (choose one) echo "ANTHROPIC_API_KEY=sk-ant-..." > docker/.env # Or: echo "OPENAI_API_KEY=sk-..." > docker/.env # Or: echo "GEMINI_API_KEY=..." > docker/.env docker-compose -f docker/docker-compose.yml up -d # Open http://localhost:9992 ``` [Full deployment guide →](https://docs.bytebot.ai/quickstart) ## How It Works Bytebot consists of four integrated components: 1. **Virtual Desktop**: A complete Ubuntu Linux environment with pre-installed applications 2. **AI Agent**: Understands your tasks and controls the desktop to complete them 3. **Task Interface**: Web UI where you create tasks and watch Bytebot work 4. **APIs**: REST endpoints for programmatic task creation and desktop control ### Key Features - **Natural Language Tasks**: Just describe what you need done - **File Uploads**: Drop files onto tasks for Bytebot to process - **Live Desktop View**: Watch Bytebot work in real-time - **Takeover Mode**: Take control when you need to help or configure something - **Password Manager Support**: Install 1Password, Bitwarden, etc. for automatic authentication - **Persistent Environment**: Install programs and they stay available for future tasks ## Example Tasks ### Basic Examples ``` "Go to Wikipedia and create a summary of quantum computing" "Research flights from NYC to London and create a comparison document" "Take screenshots of the top 5 news websites" ``` ### Document Processing ``` "Read the uploaded contracts.pdf and extract all payment terms and deadlines" "Process these 5 invoice PDFs and create a summary report" "Download and analyze the latest financial report and answer: What were the key risks mentioned?" ``` ### Multi-Application Workflows ``` "Download last month's bank statements from our three banks and consolidate them" "Check all our vendor portals for new invoices and create a summary report" "Log into our CRM, export the customer list, and update records in the ERP system" ``` ## Programmatic Control ### Create Tasks via API ```python import requests # Simple task response = requests.post('http://localhost:9991/tasks', json={ 'description': 'Download the latest sales report and create a summary' }) # Task with file upload files = {'files': open('contracts.pdf', 'rb')} response = requests.post('http://localhost:9991/tasks', data={'description': 'Review these contracts for important dates'}, files=files ) ``` ### Direct Desktop Control ```bash # Take a screenshot curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "screenshot"}' # Click at specific coordinates curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "click_mouse", "coordinate": [500, 300]}' ``` [Full API documentation →](https://docs.bytebot.ai/api-reference/introduction) ## Setting Up Your Desktop Agent ### 1. Deploy Bytebot Use one of the deployment methods above to get Bytebot running. ### 2. Configure the Desktop Use the Desktop tab in the UI to: - Install additional programs you need - Set up password managers for authentication - Configure applications with your preferences - Log into websites you want Bytebot to access ### 3. Start Giving Tasks Create tasks in natural language and watch Bytebot complete them using the configured desktop. ## Use Cases ### Business Process Automation - Invoice processing and data extraction - Multi-system data synchronization - Report generation from multiple sources - Compliance checking across platforms ### Development & Testing - Automated UI testing - Cross-browser compatibility checks - Documentation generation with screenshots - Code deployment verification ### Research & Analysis - Competitive analysis across websites - Data gathering from multiple sources - Document analysis and summarization - Market research compilation ## Architecture Bytebot is built with: - **Desktop**: Ubuntu 22.04 with XFCE, Firefox, VS Code, and other tools - **Agent**: NestJS service that coordinates AI and desktop actions - **UI**: Next.js application for task management - **AI Support**: Works with Anthropic Claude, OpenAI GPT, Google Gemini - **Deployment**: Docker containers for easy self-hosting ## Why Self-Host? - **Data Privacy**: Everything runs on your infrastructure - **Full Control**: Customize the desktop environment as needed - **No Limits**: Use your own AI API keys without platform restrictions - **Flexibility**: Install any software, access any systems ## Advanced Features ### Multiple AI Providers Use any AI provider through our [LiteLLM integration](https://docs.bytebot.ai/deployment/litellm): - Azure OpenAI - AWS Bedrock - Local models via Ollama - 100+ other providers ### Enterprise Deployment Deploy on Kubernetes with Helm: ```bash # Clone the repository git clone https://github.com/bytebot-ai/bytebot.git cd bytebot # Install with Helm helm install bytebot ./helm \ --set agent.env.ANTHROPIC_API_KEY=sk-ant-... ``` [Enterprise deployment guide →](https://docs.bytebot.ai/deployment/helm) ## Community & Support - **Discord**: [Join our community](https://discord.com/invite/d9ewZkWPTP) for help and discussions - **Documentation**: Comprehensive guides at [docs.bytebot.ai](https://docs.bytebot.ai) - **GitHub Issues**: Report bugs and request features ## Contributing We welcome contributions! Whether it's: - 🐛 Bug fixes - ✨ New features - 📚 Documentation improvements - 🌐 Translations Please: 1. Check existing [issues](https://github.com/bytebot-ai/bytebot/issues) first 2. Open an issue to discuss major changes 3. Submit PRs with clear descriptions 4. Join our [Discord](https://discord.com/invite/d9ewZkWPTP) to discuss ideas ## License Bytebot is open source under the Apache 2.0 license. ---
**Give your AI its own computer. See what it can do.** [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ) Built by [Tantl Labs](https://tantl.com) and the open source community
================================================ FILE: docker/bytebot-desktop.Dockerfile ================================================ # Extend the pre-built bytebot-desktop image FROM ghcr.io/bytebot-ai/bytebot-desktop:edge # Add additional packages, applications, or customizations here # Expose the bytebotd service port EXPOSE 9990 # Start the bytebotd service CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf", "-n"] ================================================ FILE: docker/docker-compose-claude-code.yml ================================================ name: bytebot services: bytebot-desktop: # Build from source build: context: ../packages/ dockerfile: bytebotd/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-desktop:edge shm_size: "2g" container_name: bytebot-desktop restart: unless-stopped hostname: computer privileged: true ports: - "9990:9990" # bytebotd service & noVNC environment: - DISPLAY=:0 networks: - bytebot-network postgres: image: postgres:16-alpine container_name: bytebot-postgres restart: unless-stopped ports: - "5432:5432" environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres - POSTGRES_DB=bytebotdb networks: - bytebot-network volumes: - postgres_data:/var/lib/postgresql/data bytebot-agent-cc: build: context: ../packages/ dockerfile: bytebot-agent-cc/Dockerfile container_name: bytebot-agent-cc restart: unless-stopped ports: - "9991:9991" environment: - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb} - BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} depends_on: - postgres networks: - bytebot-network bytebot-ui: build: context: ../packages/ dockerfile: bytebot-ui/Dockerfile args: - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent-cc:9991} - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify} # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-ui:edge container_name: bytebot-ui restart: unless-stopped ports: - "9992:9992" environment: - NODE_ENV=production - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent-cc:9991} - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify} depends_on: - bytebot-agent-cc networks: - bytebot-network networks: bytebot-network: driver: bridge volumes: postgres_data: ================================================ FILE: docker/docker-compose.core.yml ================================================ name: bytebot services: bytebot-desktop: # Build from source build: context: ../packages/ dockerfile: bytebotd/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-desktop:edge shm_size: "2g" container_name: bytebot-desktop restart: unless-stopped hostname: computer privileged: true ports: - "9990:9990" # bytebotd service & noVNC environment: - DISPLAY=:0 ================================================ FILE: docker/docker-compose.development.yml ================================================ ## docker-compose file that spins up a bytebot-desktop container ## and a postgres container. bytebot-ui and bytebot-agent are not included ## in this file, and can be run separately using npm, allowing for ## easier local development. name: bytebot services: bytebot-desktop: # Build from source build: context: ../packages/ dockerfile: bytebotd/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-desktop:edge shm_size: "2g" container_name: bytebot-desktop restart: unless-stopped hostname: computer privileged: true ports: - "9990:9990" # bytebotd service & noVNC environment: - DISPLAY=:0 networks: - bytebot-network postgres: image: postgres:16-alpine container_name: bytebot-postgres restart: unless-stopped ports: - "5432:5432" environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres - POSTGRES_DB=bytebotdb networks: - bytebot-network volumes: - postgres_data:/var/lib/postgresql/data networks: bytebot-network: driver: bridge volumes: postgres_data: ================================================ FILE: docker/docker-compose.proxy.yml ================================================ name: bytebot services: bytebot-desktop: # Build from source build: context: ../packages/ dockerfile: bytebotd/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-desktop:edge shm_size: "2g" container_name: bytebot-desktop restart: unless-stopped hostname: computer privileged: true ports: - "9990:9990" # bytebotd service & noVNC environment: - DISPLAY=:0 networks: - bytebot-network postgres: image: postgres:16-alpine container_name: bytebot-postgres restart: unless-stopped ports: - "5432:5432" environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres - POSTGRES_DB=bytebotdb networks: - bytebot-network volumes: - postgres_data:/var/lib/postgresql/data bytebot-agent: build: context: ../packages/ dockerfile: bytebot-agent/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-agent:edge container_name: bytebot-agent restart: unless-stopped ports: - "9991:9991" environment: - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb} - BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990} - BYTEBOT_LLM_PROXY_URL=${BYTEBOT_LLM_PROXY_URL:-http://bytebot-llm-proxy:4000} depends_on: - postgres networks: - bytebot-network bytebot-llm-proxy: build: context: ../packages/ dockerfile: bytebot-llm-proxy/Dockerfile ports: - "4000:4000" environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} - GEMINI_API_KEY=${GEMINI_API_KEY} networks: - bytebot-network bytebot-ui: build: context: ../packages/ dockerfile: bytebot-ui/Dockerfile args: - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991} - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify} # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-ui:edge container_name: bytebot-ui restart: unless-stopped ports: - "9992:9992" environment: - NODE_ENV=production - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991} - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify} depends_on: - bytebot-agent networks: - bytebot-network networks: bytebot-network: driver: bridge volumes: postgres_data: ================================================ FILE: docker/docker-compose.yml ================================================ name: bytebot services: bytebot-desktop: # Build from source build: context: ../packages/ dockerfile: bytebotd/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-desktop:edge shm_size: "2g" container_name: bytebot-desktop restart: unless-stopped hostname: computer privileged: true ports: - "9990:9990" # bytebotd service & noVNC environment: - DISPLAY=:0 networks: - bytebot-network postgres: image: postgres:16-alpine container_name: bytebot-postgres restart: unless-stopped ports: - "5432:5432" environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres - POSTGRES_DB=bytebotdb networks: - bytebot-network volumes: - postgres_data:/var/lib/postgresql/data bytebot-agent: build: context: ../packages/ dockerfile: bytebot-agent/Dockerfile # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-agent:edge container_name: bytebot-agent restart: unless-stopped ports: - "9991:9991" environment: - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb} - BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} - GEMINI_API_KEY=${GEMINI_API_KEY} depends_on: - postgres networks: - bytebot-network bytebot-ui: build: context: ../packages/ dockerfile: bytebot-ui/Dockerfile args: - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991} - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify} # Use pre-built image image: ghcr.io/bytebot-ai/bytebot-ui:edge container_name: bytebot-ui restart: unless-stopped ports: - "9992:9992" environment: - NODE_ENV=production - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991} - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify} depends_on: - bytebot-agent networks: - bytebot-network networks: bytebot-network: driver: bridge volumes: postgres_data: ================================================ FILE: docs/api-reference/agent/tasks.mdx ================================================ --- title: 'Tasks API' description: 'Reference documentation for the Bytebot Agent Tasks API' --- ## Tasks API The Tasks API allows you to manage tasks in the Bytebot agent system. It's available at `http://localhost:9991/tasks` when running the full agent setup. ## Task Model ```typescript { id: string; description: string; status: 'PENDING' | 'IN_PROGRESS' | 'NEEDS_HELP' | 'NEEDS_REVIEW' | 'COMPLETED' | 'CANCELLED' | 'FAILED'; priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'; createdAt: string; updatedAt: string; } ``` ## Endpoints ### Create Task Create a new task for the agent to process. Create a new task #### Request Body ```json { "description": "This is a description of the task", "priority": "MEDIUM" // Optional: LOW, MEDIUM, HIGH, URGENT } ``` #### With File Upload To upload files with a task, use `multipart/form-data`: ```bash curl -X POST http://localhost:9991/tasks \ -F "description=Analyze the uploaded contracts and extract key terms" \ -F "priority=HIGH" \ -F "files=@contract1.pdf" \ -F "files=@contract2.pdf" ``` Uploaded files are automatically saved to the desktop and can be referenced in the task description. #### Response ```json { "id": "task-123", "description": "This is a description of the task", "status": "PENDING", "priority": "MEDIUM", "createdAt": "2025-04-14T12:00:00Z", "updatedAt": "2025-04-14T12:00:00Z" } ``` ### Get All Tasks Retrieve a list of all tasks. Get all tasks #### Response ```json [ { "id": "task-123", "description": "This is a description of the task", "status": "PENDING", "priority": "MEDIUM", "createdAt": "2025-04-14T12:00:00Z", "updatedAt": "2025-04-14T12:00:00Z" }, // ...more tasks ] ``` ### Get In-Progress Task Retrieve the currently in-progress task, if any. Get the currently in-progress task #### Response ```json { "id": "task-123", "description": "This is a description of the task", "status": "IN_PROGRESS", "priority": "MEDIUM", "createdAt": "2025-04-14T12:00:00Z", "updatedAt": "2025-04-14T12:00:00Z" } ``` If no task is in progress, the response will be `null`. ### Get Task by ID Retrieve a specific task by its ID. Get a task by ID #### Response ```json { "id": "task-123", "description": "This is a description of the task", "status": "PENDING", "priority": "MEDIUM", "createdAt": "2025-04-14T12:00:00Z", "updatedAt": "2025-04-14T12:00:00Z", "messages": [ { "id": "msg-456", "content": [ { "type": "text", "text": "This is a message" } ], "role": "USER", "taskId": "task-123", "createdAt": "2025-04-14T12:05:00Z", "updatedAt": "2025-04-14T12:05:00Z" } // ...more messages ] } ``` ### Update Task Update an existing task. Update a task #### Request Body ```json { "status": "COMPLETED", "priority": "HIGH" } ``` #### Response ```json { "id": "task-123", "description": "This is a description of the task", "status": "COMPLETED", "priority": "HIGH", "createdAt": "2025-04-14T12:00:00Z", "updatedAt": "2025-04-14T12:01:00Z" } ``` ### Delete Task Delete a task. Delete a task #### Response Status code `204 No Content` with an empty response body. ## Message Content Structure Messages in the Bytebot agent system use a content block structure compatible with Anthropic's Claude API: ```typescript type MessageContent = MessageContentBlock[]; interface MessageContentBlock { type: string; [key: string]: any; } interface TextContentBlock { type: "text"; text: string; } interface ImageContentBlock { type: "image"; source: { type: "base64"; media_type: string; data: string; }; } ``` ## Error Responses The API may return the following error responses: | Status Code | Description | |-------------|--------------------------------------------| | `400` | Bad Request - Invalid parameters | | `404` | Not Found - Resource does not exist | | `500` | Internal Server Error - Server side error | Example error response: ```json { "statusCode": 404, "message": "Task with ID task-123 not found", "error": "Not Found" } ``` ## Code Examples ```javascript JavaScript const axios = require('axios'); async function createTask(description) { const response = await axios.post('http://localhost:9991/tasks', { description }); return response.data; } async function findInProgressTask() { const response = await axios.get('http://localhost:9991/tasks/in-progress'); return response.data; } // Example usage async function main() { // Create a new task const task = await createTask('Compare React, Vue, and Angular for a new project'); console.log('Created task:', task); // Get current in-progress task const inProgressTask = await findInProgressTask(); console.log('In progress task:', inProgressTask); } ``` ```python Python import requests def create_task(description): response = requests.post( "http://localhost:9991/tasks", json={ "description": description } ) return response.json() def find_in_progress_task(): response = requests.get("http://localhost:9991/tasks/in-progress") return response.json() # Example usage def main(): # Create a new task task = create_task("Compare React, Vue, and Angular for a new project") print(f"Created task: {task}") # Get current in-progress task in_progress_task = find_in_progress_task() print(f"In progress task: {in_progress_task}") ``` ```curl cURL # Create a new task curl -X POST http://localhost:9991/tasks \ -H "Content-Type: application/json" \ -d '{ "description": "Compare React, Vue, and Angular for a new project" }' # Get current in-progress task curl -X GET http://localhost:9991/tasks/in-progress ``` ================================================ FILE: docs/api-reference/agent/ui.mdx ================================================ --- title: 'Task UI' description: 'Documentation for the Bytebot Task UI' --- ## Bytebot Task UI The Bytebot Task UI provides a web-based interface for interacting with the Bytebot agent system. It combines a action feed with an embedded noVNC viewer, allowing you to watch it perform task on the desktop in real-time. Bytebot Task Detail ## Accessing the UI When running the full Bytebot agent system, the Task UI is available at: ``` http://localhost:9992 ``` ## UI Components ### Task Management Panel The task management panel allows you to: - Create new tasks - View existing tasks - See task status and priority - Select a task to work on Task Management Panel ### Task Interface The main task interface provides: - Task history with the agent - Support for markdown formatting in messages - Automatic scrolling to new messages ### Desktop Viewer The embedded noVNC viewer displays: - Real-time view of the desktop environment - Visual feedback of agent actions - Option to expand to take over the desktop - Connection status indicator ## Features ### Task Creation To create a new task: 1. Enter a description for the task 2. Click "Start Task" button (or press Enter) ### Conversation Controls The task interface supports: - Text messages with markdown formatting - Viewing image content in messages - Displaying tool use actions - Showing tool results ### Desktop Interaction While primarily for viewing, the desktop panel allows: - Taking over the desktop - Real-time monitoring of agent actions ## Message Types The task interface displays different types of messages based on Bytebot's content block structure: - **User Messages**: Your instructions and queries - **Assistant Messages**: Responses from the agent, which may include: - **Text Content Blocks**: Markdown-formatted text responses - **Image Content Blocks**: Images generated or captured - **Tool Use Content Blocks**: Computer actions being performed - **Tool Result Content Blocks**: Results of computer actions The message content structure follows this format: ```typescript interface Message { id: string; content: MessageContentBlock[]; role: Role; // "USER" or "ASSISTANT" createdAt?: string; } interface MessageContentBlock { type: string; [key: string]: any; } interface TextContentBlock extends MessageContentBlock { type: "text"; text: string; } interface ImageContentBlock extends MessageContentBlock { type: "image"; source: { type: "base64"; media_type: string; data: string; }; } ``` ## Technical Details The Bytebot Task UI is built with: - **Next.js**: React framework for the frontend - **Tailwind CSS**: For styling - **ReactMarkdown**: For rendering markdown content - **noVNC**: For the embedded desktop viewer ## Troubleshooting ### Connection Issues If you experience connection issues: 1. Ensure all Bytebot services are running 2. Check that ports 9990, 9991, and 9992 are accessible 3. Try refreshing the browser 4. Check browser console for error messages ### Desktop Viewer Issues If the desktop viewer is not displaying: 1. Ensure the Bytebot container is running 2. Check that the noVNC service is accessible at port 9990 ### Message Display Issues If messages are not displaying correctly: 1. Check that the message content is properly formatted 2. Ensure the agent service is processing task correctly 3. Check the browser console for any rendering errors 4. Try refreshing the browser ================================================ FILE: docs/api-reference/computer-use/examples.mdx ================================================ --- title: "Computer Use API Examples" description: "Code examples for common automation scenarios using the Bytebot API" --- ## Basic Examples Here are some practical examples of how to use the Computer Use API in different programming languages. ### Using cURL ```bash Opening a Web Browser # Move to Firefox/Chrome icon in the dock and click it curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "move_mouse", "coordinates": {"x": 100, "y": 960}}' curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "click_mouse", "button": "left", "clickCount": 1}' ```` ```bash Taking and Saving a Screenshot # Take a screenshot response=$(curl -s -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "screenshot"}') # Extract the base64 image data and save to a file echo $response | jq -r '.data.image' | base64 -d > screenshot.png ```` ```bash Typing and Keyboard Shortcuts # Type text in a text editor curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "type_text", "text": "Hello, this is an automated test!", "delay": 30}' # Press Ctrl+S to save curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "press_keys", "key": "s", "modifiers": ["control"]}' ``` ### Python Examples ```python Basic Automation import requests import json import base64 import time from io import BytesIO from PIL import Image def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() # Open a web browser by clicking an icon control_computer("move_mouse", coordinates={"x": 100, "y": 960}) control_computer("click_mouse", button="left") # Wait for the browser to open control_computer("wait", duration=2000) # Type a URL control_computer("type_text", text="https://example.com") control_computer("press_keys", key="enter") ```` ```python Screenshot and Analysis import requests import json import base64 import cv2 import numpy as np from PIL import Image from io import BytesIO def take_screenshot(): url = "http://localhost:9990/computer-use" data = {"action": "screenshot"} response = requests.post(url, json=data) if response.json()["success"]: img_data = base64.b64decode(response.json()["data"]["image"]) image = Image.open(BytesIO(img_data)) return np.array(image) return None # Take a screenshot img = take_screenshot() # Convert to grayscale for analysis if img is not None: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Save the screenshot cv2.imwrite("screenshot.png", img) # Perform image analysis (example: find edges) edges = cv2.Canny(gray, 100, 200) cv2.imwrite("edges.png", edges) ```` ```python Web Form Automation import requests import time def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def fill_web_form(form_fields): # Click on the first form field control_computer("move_mouse", coordinates=form_fields[0]) control_computer("click_mouse", button="left") # Fill out each field for i, field in enumerate(form_fields): # Input the field value control_computer("type_text", text=field["value"]) # If not the last field, press Tab to move to next field if i < len(form_fields) - 1: control_computer("press_keys", key="tab") time.sleep(0.5) # Submit the form by pressing Enter control_computer("press_keys", key="enter") # Example form fields with coordinates and values form_fields = [ {"x": 500, "y": 300, "value": "John Doe"}, {"x": 500, "y": 350, "value": "john@example.com"}, {"x": 500, "y": 400, "value": "Password123"} ] fill_web_form(form_fields) ``` ### JavaScript/Node.js Examples ```javascript Basic Automation const axios = require('axios'); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; try { const response = await axios.post(url, data); return response.data; } catch (error) { console.error('Error:', error.message); return { success: false, error: error.message }; } } // Example: Automate opening an application and typing async function automateTextEditor() { try { // Open text editor by clicking its icon await controlComputer("move_mouse", { coordinates: { x: 150, y: 960 } }); await controlComputer("click_mouse", { button: "left" }); // Wait for it to open await controlComputer("wait", { duration: 2000 }); // Type some text await controlComputer("type_text", { text: "This is an automated test using Node.js and Bytebot", delay: 30 }); console.log("Automation completed successfully"); } catch (error) { console.error("Automation failed:", error); } } automateTextEditor(); ```` ```javascript Advanced: Screenshot Comparison const axios = require('axios'); const fs = require('fs'); const { createCanvas, loadImage } = require('canvas'); const pixelmatch = require('pixelmatch'); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; try { const response = await axios.post(url, data); return response.data; } catch (error) { console.error('Error:', error.message); return { success: false, error: error.message }; } } async function compareScreenshots() { try { // Take first screenshot const screenshot1 = await controlComputer("screenshot"); // Do some actions await controlComputer("move_mouse", { coordinates: { x: 500, y: 500 } }); await controlComputer("click_mouse", { button: "left" }); await controlComputer("wait", { duration: 1000 }); // Take second screenshot const screenshot2 = await controlComputer("screenshot"); // Compare screenshots if (screenshot1.success && screenshot2.success) { const img1Data = Buffer.from(screenshot1.data.image, 'base64'); const img2Data = Buffer.from(screenshot2.data.image, 'base64'); fs.writeFileSync('screenshot1.png', img1Data); fs.writeFileSync('screenshot2.png', img2Data); // Now you could load and compare these images // This requires additional image comparison libraries console.log('Screenshots saved for comparison'); } } catch (error) { console.error("Screenshot comparison failed:", error); } } compareScreenshots(); ```` ## File Operations ### Writing Files These examples show how to write files to the desktop environment: ```python Python import requests import base64 def write_file(path, content): url = "http://localhost:9990/computer-use" # Encode content to base64 encoded_content = base64.b64encode(content.encode('utf-8')).decode('utf-8') data = { "action": "write_file", "path": path, "data": encoded_content } response = requests.post(url, json=data) return response.json() # Write a text file result = write_file("/home/user/hello.txt", "Hello, Bytebot!") print(result) # {'success': True, 'message': 'File written successfully...'} # Write to desktop (relative path) result = write_file("report.txt", "Daily report content") print(result) # File will be written to /home/user/Desktop/report.txt ``` ```javascript JavaScript const axios = require('axios'); async function writeFile(path, content) { const url = "http://localhost:9990/computer-use"; // Encode content to base64 const encodedContent = Buffer.from(content, 'utf-8').toString('base64'); const data = { action: "write_file", path: path, data: encodedContent }; const response = await axios.post(url, data); return response.data; } // Write a text file writeFile("/home/user/notes.txt", "Meeting notes...") .then(result => console.log(result)) .catch(error => console.error(error)); // Write HTML file to desktop const htmlContent = '

Hello

'; writeFile("index.html", htmlContent) .then(result => console.log("HTML file created")); ```
### Reading Files These examples show how to read files from the desktop environment: ```python Python import requests import base64 def read_file(path): url = "http://localhost:9990/computer-use" data = { "action": "read_file", "path": path } response = requests.post(url, json=data) result = response.json() if result['success']: # Decode the base64 content content = base64.b64decode(result['data']).decode('utf-8') return { 'content': content, 'name': result['name'], 'size': result['size'], 'mediaType': result['mediaType'] } else: return result # Read a text file file_data = read_file("/home/user/hello.txt") print(f"Content: {file_data['content']}") print(f"Size: {file_data['size']} bytes") print(f"Type: {file_data['mediaType']}") ``` ```javascript JavaScript const axios = require('axios'); async function readFile(path) { const url = "http://localhost:9990/computer-use"; const data = { action: "read_file", path: path }; const response = await axios.post(url, data); const result = response.data; if (result.success) { // Decode the base64 content const content = Buffer.from(result.data, 'base64').toString('utf-8'); return { content: content, name: result.name, size: result.size, mediaType: result.mediaType }; } else { throw new Error(result.message); } } // Read a file from desktop readFile("report.txt") .then(fileData => { console.log(`Content: ${fileData.content}`); console.log(`Size: ${fileData.size} bytes`); console.log(`Type: ${fileData.mediaType}`); }) .catch(error => console.error("Error reading file:", error)); ``` ## Automation Recipes ### Browser Automation This example demonstrates how to automate browser interactions: ```python import requests import time def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def automate_browser(): # Open browser (assuming browser icon is at position x=100, y=960) control_computer("move_mouse", coordinates={"x": 100, "y": 960}) control_computer("click_mouse", button="left") time.sleep(3) # Wait for browser to open # Type URL control_computer("type_text", text="https://example.com") control_computer("press_keys", key="enter") time.sleep(2) # Wait for page to load # Take screenshot of the loaded page screenshot = control_computer("screenshot") # Click on a link (coordinates would need to be adjusted for your target) control_computer("move_mouse", coordinates={"x": 300, "y": 400}) control_computer("click_mouse", button="left") time.sleep(2) # Scroll down control_computer("scroll", direction="down", scrollCount=5) automate_browser() ``` ### Form Filling Automation This example shows how to automate filling out a form in a web application: ```javascript const axios = require("axios"); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; const response = await axios.post(url, data); return response.data; } async function fillForm() { // Click first input field await controlComputer("move_mouse", { coordinates: { x: 400, y: 300 } }); await controlComputer("click_mouse", { button: "left" }); // Type name await controlComputer("type_text", { text: "John Doe" }); // Tab to next field await controlComputer("press_keys", { key: "tab" }); // Type email await controlComputer("type_text", { text: "john@example.com" }); // Tab to next field await controlComputer("press_keys", { key: "tab" }); // Type message await controlComputer("type_text", { text: "This is an automated message sent using Bytebot's Computer Use API", delay: 30, }); // Tab to submit button await controlComputer("press_keys", { key: "tab" }); // Press Enter to submit await controlComputer("press_keys", { key: "enter" }); } fillForm().catch(console.error); ``` ## Integration with Testing Frameworks The Computer Use API can be integrated with popular testing frameworks: ### Selenium Alternative Bytebot can serve as an alternative to Selenium for web testing: ```python import requests import time import json class BytebotWebDriver: def __init__(self, base_url="http://localhost:9990"): self.base_url = base_url def control_computer(self, action, **params): url = f"{self.base_url}/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def open_browser(self, browser_icon_coords): self.control_computer("move_mouse", coordinates=browser_icon_coords) self.control_computer("click_mouse", button="left") time.sleep(3) # Wait for browser to open def navigate_to(self, url): self.control_computer("type_text", text=url) self.control_computer("press_keys", key="enter") time.sleep(2) # Wait for page to load def click_element(self, coords): self.control_computer("move_mouse", coordinates=coords) self.control_computer("click_mouse", button="left") def type_text(self, text): self.control_computer("type_text", text=text) def press_keys(self, key, modifiers=None): params = {"key": key} if modifiers: params["modifiers"] = modifiers self.control_computer("press_keys", **params) def take_screenshot(self): return self.control_computer("screenshot") # Usage example driver = BytebotWebDriver() driver.open_browser({"x": 100, "y": 960}) driver.navigate_to("https://example.com") driver.click_element({"x": 300, "y": 400}) driver.type_text("Hello Bytebot!") ``` ================================================ FILE: docs/api-reference/computer-use/openapi.json ================================================ { "openapi": "3.1.0", "info": { "title": "Bytebot Computer Use API", "version": "1.0.0", "description": "Control the Bytebot virtual desktop via a single endpoint" }, "paths": { "/computer-use": { "post": { "summary": "Execute a computer action", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComputerAction" } } } }, "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComputerActionResponse" } } } }, "500": { "description": "Error executing action", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": {"type": "string"}, "error": {"type": "string"} } } } } } } } } }, "components": { "schemas": { "Coordinates": { "type": "object", "properties": { "x": {"type": "number"}, "y": {"type": "number"} }, "required": ["x", "y"] }, "Button": { "type": "string", "enum": ["left", "right", "middle"] }, "Press": { "type": "string", "enum": ["up", "down"] }, "ScrollDirection": { "type": "string", "enum": ["up", "down", "left", "right"] }, "MoveMouseAction": { "type": "object", "properties": { "action": {"enum": ["move_mouse"]}, "coordinates": {"$ref": "#/components/schemas/Coordinates"} }, "required": ["action", "coordinates"] }, "TraceMouseAction": { "type": "object", "properties": { "action": {"enum": ["trace_mouse"]}, "path": { "type": "array", "items": {"$ref": "#/components/schemas/Coordinates"} }, "holdKeys": { "type": "array", "items": {"type": "string"} } }, "required": ["action", "path"] }, "ClickMouseAction": { "type": "object", "properties": { "action": {"enum": ["click_mouse"]}, "coordinates": {"$ref": "#/components/schemas/Coordinates"}, "button": {"$ref": "#/components/schemas/Button"}, "holdKeys": { "type": "array", "items": {"type": "string"} }, "clickCount": {"type": "integer", "minimum": 1} }, "required": ["action", "button", "clickCount"] }, "PressMouseAction": { "type": "object", "properties": { "action": {"enum": ["press_mouse"]}, "coordinates": {"$ref": "#/components/schemas/Coordinates"}, "button": {"$ref": "#/components/schemas/Button"}, "press": {"$ref": "#/components/schemas/Press"} }, "required": ["action", "button", "press"] }, "DragMouseAction": { "type": "object", "properties": { "action": {"enum": ["drag_mouse"]}, "path": { "type": "array", "items": {"$ref": "#/components/schemas/Coordinates"} }, "button": {"$ref": "#/components/schemas/Button"}, "holdKeys": { "type": "array", "items": {"type": "string"} } }, "required": ["action", "path", "button"] }, "ScrollAction": { "type": "object", "properties": { "action": {"enum": ["scroll"]}, "coordinates": {"$ref": "#/components/schemas/Coordinates"}, "direction": {"$ref": "#/components/schemas/ScrollDirection"}, "scrollCount": {"type": "integer", "minimum": 1}, "holdKeys": { "type": "array", "items": {"type": "string"} } }, "required": ["action", "direction", "scrollCount"] }, "TypeKeysAction": { "type": "object", "properties": { "action": {"enum": ["type_keys"]}, "keys": { "type": "array", "items": {"type": "string"} }, "delay": {"type": "integer", "minimum": 0} }, "required": ["action", "keys"] }, "PressKeysAction": { "type": "object", "properties": { "action": {"enum": ["press_keys"]}, "keys": { "type": "array", "items": {"type": "string"} }, "press": {"$ref": "#/components/schemas/Press"} }, "required": ["action", "keys", "press"] }, "TypeTextAction": { "type": "object", "properties": { "action": {"enum": ["type_text"]}, "text": {"type": "string"}, "delay": {"type": "integer", "minimum": 0} }, "required": ["action", "text"] }, "WaitAction": { "type": "object", "properties": { "action": {"enum": ["wait"]}, "duration": {"type": "integer", "minimum": 0} }, "required": ["action", "duration"] }, "ScreenshotAction": { "type": "object", "properties": { "action": {"enum": ["screenshot"]} }, "required": ["action"] }, "CursorPositionAction": { "type": "object", "properties": { "action": {"enum": ["cursor_position"]} }, "required": ["action"] }, "ComputerAction": { "oneOf": [ {"$ref": "#/components/schemas/MoveMouseAction"}, {"$ref": "#/components/schemas/TraceMouseAction"}, {"$ref": "#/components/schemas/ClickMouseAction"}, {"$ref": "#/components/schemas/PressMouseAction"}, {"$ref": "#/components/schemas/DragMouseAction"}, {"$ref": "#/components/schemas/ScrollAction"}, {"$ref": "#/components/schemas/TypeKeysAction"}, {"$ref": "#/components/schemas/PressKeysAction"}, {"$ref": "#/components/schemas/TypeTextAction"}, {"$ref": "#/components/schemas/WaitAction"}, {"$ref": "#/components/schemas/ScreenshotAction"}, {"$ref": "#/components/schemas/CursorPositionAction"} ], "discriminator": { "propertyName": "action", "mapping": { "move_mouse": "#/components/schemas/MoveMouseAction", "trace_mouse": "#/components/schemas/TraceMouseAction", "click_mouse": "#/components/schemas/ClickMouseAction", "press_mouse": "#/components/schemas/PressMouseAction", "drag_mouse": "#/components/schemas/DragMouseAction", "scroll": "#/components/schemas/ScrollAction", "type_keys": "#/components/schemas/TypeKeysAction", "press_keys": "#/components/schemas/PressKeysAction", "type_text": "#/components/schemas/TypeTextAction", "wait": "#/components/schemas/WaitAction", "screenshot": "#/components/schemas/ScreenshotAction", "cursor_position": "#/components/schemas/CursorPositionAction" } } }, "ScreenshotResponse": { "type": "object", "properties": { "image": { "type": "string", "description": "Base64 encoded PNG" } }, "required": ["image"] }, "CursorPosition": { "type": "object", "properties": { "x": {"type": "number"}, "y": {"type": "number"} }, "required": ["x", "y"] }, "ComputerActionResponse": { "type": "object", "properties": { "success": {"type": "boolean"}, "data": { "oneOf": [ {"$ref": "#/components/schemas/ScreenshotResponse"}, {"$ref": "#/components/schemas/CursorPosition"} ] } }, "required": ["success"] } } } } ================================================ FILE: docs/api-reference/computer-use/unified-endpoint.mdx ================================================ --- title: "Unified Computer Actions API" description: "Control all aspects of the desktop environment with a single endpoint" --- ## Overview The unified computer action API allows for granular control over all aspects of the Bytebot virtual desktop environment through a single endpoint. It replaces multiple specific endpoints with a unified interface that handles various computer actions like mouse movements, clicks, key presses, and more. ## Endpoint | Method | URL | Description | | ------ | ---------------- | ----------------------------------------------- | | POST | `/computer-use` | Execute computer actions in the virtual desktop | ## Request Format All requests to the unified endpoint follow this format: ```json { "action": "action_name", ...action-specific parameters } ``` The `action` parameter determines which operation to perform, and the remaining parameters depend on the specific action. ## Available Actions ### move_mouse Move the mouse cursor to a specific position. **Parameters:** | Parameter | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------- | | `coordinates` | Object | Yes | The target coordinates to move to | | `coordinates.x` | Number | Yes | X coordinate | | `coordinates.y` | Number | Yes | Y coordinate | **Example:** ```json { "action": "move_mouse", "coordinates": { "x": 100, "y": 200 } } ``` ### trace_mouse Move the mouse along a path of coordinates. **Parameters:** | Parameter | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------- | | `path` | Array | Yes | Array of coordinate objects for the mouse path | | `path[].x` | Number | Yes | X coordinate for each point in the path | | `path[].y` | Number | Yes | Y coordinate for each point in the path | | `holdKeys` | Array | No | Keys to hold while moving along the path | **Example:** ```json { "action": "trace_mouse", "path": [ { "x": 100, "y": 100 }, { "x": 150, "y": 150 }, { "x": 200, "y": 200 } ], "holdKeys": ["shift"] } ``` ### click_mouse Perform a mouse click at the current or specified position. **Parameters:** | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------------------- | | `coordinates` | Object | No | The coordinates to click (uses current if omitted) | | `coordinates.x` | Number | Yes* | X coordinate | | `coordinates.y` | Number | Yes* | Y coordinate | | `button` | String | Yes | Mouse button: 'left', 'right', or 'middle' | | `clickCount` | Number | Yes | Number of clicks to perform | | `holdKeys` | Array | No | Keys to hold while clicking (e.g., ['ctrl', 'shift']) | **Example:** ```json { "action": "click_mouse", "coordinates": { "x": 150, "y": 250 }, "button": "left", "clickCount": 2 } ``` ### press_mouse Press or release a mouse button at the current or specified position. **Parameters:** | Parameter | Type | Required | Description | | --------------- | ------ | -------- | -------------------------------------------------------- | | `coordinates` | Object | No | The coordinates to press/release (uses current if omitted) | | `coordinates.x` | Number | Yes* | X coordinate | | `coordinates.y` | Number | Yes* | Y coordinate | | `button` | String | Yes | Mouse button: 'left', 'right', or 'middle' | | `press` | String | Yes | Action: 'up' or 'down' | **Example:** ```json { "action": "press_mouse", "coordinates": { "x": 150, "y": 250 }, "button": "left", "press": "down" } ``` ### drag_mouse Click and drag the mouse from one point to another. **Parameters:** | Parameter | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------- | | `path` | Array | Yes | Array of coordinate objects for the drag path | | `path[].x` | Number | Yes | X coordinate for each point in the path | | `path[].y` | Number | Yes | Y coordinate for each point in the path | | `button` | String | Yes | Mouse button: 'left', 'right', or 'middle' | | `holdKeys` | Array | No | Keys to hold while dragging | **Example:** ```json { "action": "drag_mouse", "path": [ { "x": 100, "y": 100 }, { "x": 200, "y": 200 } ], "button": "left" } ``` ### scroll Scroll up, down, left, or right. **Parameters:** | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------ | | `coordinates` | Object | No | The coordinates to scroll at (uses current if omitted) | | `coordinates.x` | Number | Yes* | X coordinate | | `coordinates.y` | Number | Yes* | Y coordinate | | `direction` | String | Yes | Scroll direction: 'up', 'down', 'left', 'right' | | `scrollCount` | Number | Yes | Number of scroll steps | | `holdKeys` | Array | No | Keys to hold while scrolling | **Example:** ```json { "action": "scroll", "direction": "down", "scrollCount": 5 } ``` ### type_keys Type a sequence of keyboard keys. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------- | | `keys` | Array | Yes | Array of keys to type in sequence | | `delay` | Number | No | Delay between key presses (ms) | **Example:** ```json { "action": "type_keys", "keys": ["a", "b", "c", "enter"], "delay": 50 } ``` ### press_keys Press or release keyboard keys. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `keys` | Array | Yes | Array of keys to press or release | | `press` | String | Yes | Action: 'up' or 'down' | **Example:** ```json { "action": "press_keys", "keys": ["ctrl", "shift", "esc"], "press": "down" } ``` ### type_text Type a text string with optional delay. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------------------------- | | `text` | String | Yes | The text to type | | `delay` | Number | No | Delay between characters in milliseconds (default: 0) | **Example:** ```json { "action": "type_text", "text": "Hello, Bytebot!", "delay": 50 } ``` ### paste_text Paste text to the current cursor position. This is especially useful for special characters that aren't on the standard keyboard. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------ | | `text` | String | Yes | The text to paste, including special characters and emojis | **Example:** ```json { "action": "paste_text", "text": "Special characters: ©®™€¥£ émojis 🎉" } ``` ### wait Wait for a specified duration. **Parameters:** | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------- | | `duration` | Number | Yes | Wait duration in milliseconds | **Example:** ```json { "action": "wait", "duration": 2000 } ``` ### screenshot Capture a screenshot of the desktop. **Parameters:** None required **Example:** ```json { "action": "screenshot" } ``` ### cursor_position Get the current position of the mouse cursor. **Parameters:** None required **Example:** ```json { "action": "cursor_position" } ``` ### application Switch between different applications or navigate to the desktop/directory. **Parameters:** | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------------ | | `application` | String | Yes | The application to switch to. See available options below. | **Available Applications:** - `firefox` - Mozilla Firefox web browser - `1password` - Password manager - `thunderbird` - Email client - `vscode` - Visual Studio Code editor - `terminal` - Terminal/console application - `desktop` - Switch to desktop - `directory` - File manager/directory browser **Example:** ```json { "action": "application", "application": "firefox" } ``` ### write_file Write a file to the desktop environment filesystem. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------- | | `path` | String | Yes | File path (absolute or relative to /home/user/Desktop) | | `data` | String | Yes | Base64 encoded file content | **Example:** ```json { "action": "write_file", "path": "/home/user/documents/example.txt", "data": "SGVsbG8gV29ybGQh" } ``` ### read_file Read a file from the desktop environment filesystem. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------- | | `path` | String | Yes | File path (absolute or relative to /home/user/Desktop) | **Example:** ```json { "action": "read_file", "path": "/home/user/documents/example.txt" } ``` ## Response Format The response format varies depending on the action performed. ### Standard Response Most actions return a simple success response: ```json { "success": true } ``` ### Screenshot Response ```json { "success": true, "data": { "image": "base64_encoded_image_data" } } ``` ### Cursor Position Response ```json { "success": true, "data": { "x": 123, "y": 456 } } ``` ### Write File Response ```json { "success": true, "message": "File written successfully to: /home/user/documents/example.txt" } ``` ### Read File Response ```json { "success": true, "data": "SGVsbG8gV29ybGQh", "name": "example.txt", "size": 12, "mediaType": "text/plain" } ``` ### Error Response ```json { "success": false, "error": "Error message" } ``` ## Code Examples ### JavaScript/Node.js Example ```javascript const axios = require('axios'); const bytebot = { baseUrl: 'http://localhost:9990/computer-use/computer', async action(params) { try { const response = await axios.post(this.baseUrl, params); return response.data; } catch (error) { console.error('Error:', error.response?.data || error.message); throw error; } }, // Convenience methods async moveMouse(x, y) { return this.action({ action: 'move_mouse', coordinates: { x, y } }); }, async clickMouse(x, y, button = 'left') { return this.action({ action: 'click_mouse', coordinates: { x, y }, button }); }, async typeText(text) { return this.action({ action: 'type_text', text }); }, async pasteText(text) { return this.action({ action: 'paste_text', text }); }, async switchApplication(application) { return this.action({ action: 'application', application }); }, async screenshot() { return this.action({ action: 'screenshot' }); } }; // Example usage: async function example() { // Switch to Firefox await bytebot.switchApplication('firefox'); // Navigate to a website await bytebot.moveMouse(100, 35); await bytebot.clickMouse(100, 35); await bytebot.typeText('https://example.com'); await bytebot.action({ action: 'press_keys', keys: ['enter'], press: 'down' }); // Wait for page to load await bytebot.action({ action: 'wait', duration: 2000 }); // Paste some special characters await bytebot.pasteText('© 2025 Example Corp™ - €100'); // Take a screenshot const result = await bytebot.screenshot(); console.log('Screenshot taken!'); } example().catch(console.error); ================================================ FILE: docs/api-reference/endpoint/create.mdx ================================================ --- title: 'Create Plant' openapi: 'POST /plants' --- ================================================ FILE: docs/api-reference/endpoint/delete.mdx ================================================ --- title: 'Delete Plant' openapi: 'DELETE /plants/{id}' --- ================================================ FILE: docs/api-reference/endpoint/get.mdx ================================================ --- title: 'Get Plants' openapi: 'GET /plants' --- ================================================ FILE: docs/api-reference/endpoint/webhook.mdx ================================================ --- title: 'New Plant' openapi: 'WEBHOOK /plant/webhook' --- ================================================ FILE: docs/api-reference/introduction.mdx ================================================ --- title: "API Reference" description: "Overview of the Bytebot API endpoints for programmatic control" --- # Bytebot API Overview Bytebot provides two main APIs for programmatic control: ## 1. Agent API (Task Management) The Agent API runs on port 9991 and provides high-level task management: Create, manage, and monitor AI-powered tasks programmatically WebSocket connections and real-time updates for custom UIs ### Agent API Base URL ``` http://localhost:9991 ``` ### Example Task Creation ```bash curl -X POST http://localhost:9991/tasks \ -H "Content-Type: application/json" \ -d '{ "description": "Download invoices from webmail and organize by date", "priority": "HIGH" }' ``` ## 2. Desktop API (Direct Control) The Desktop API runs on port 9990 and provides low-level desktop control: Direct control of mouse, keyboard, and screen capture Code examples for common automation scenarios ### Desktop API Base URL ``` http://localhost:9990 ``` ### Example Desktop Control ```bash curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "screenshot"}' ``` ### MCP Support The Desktop API also exposes an MCP (Model Context Protocol) endpoint: ``` http://localhost:9990/mcp ``` Connect your MCP client to access desktop control tools over SSE. ## Authentication - **Local Access**: No authentication required by default - **Remote Access**: Configure authentication based on your security requirements - **Production**: Implement API keys, OAuth, or other authentication methods ## Response Formats ### Agent API Response ```json { "id": "task-123", "status": "RUNNING", "description": "Your task description", "messages": [...], "createdAt": "2024-01-01T00:00:00Z" } ``` ### Desktop API Response ```json { "success": true, "data": { ... }, // Response data specific to the action "error": null // Error message if success is false } ``` ## Error Handling Both APIs use standard HTTP status codes: | Status Code | Description | | ----------- | ------------------------------------ | | 200 | Success | | 201 | Created (new resource) | | 400 | Bad Request - Invalid parameters | | 401 | Unauthorized - Authentication failed | | 404 | Not Found - Resource doesn't exist | | 500 | Internal Server Error | ## Rate Limiting - **Agent API**: No hard limits, but consider task queue capacity - **Desktop API**: No rate limiting, but rapid actions may impact desktop performance ## Best Practices 1. **Use Agent API for high-level automation** - Let the AI handle complexity 2. **Use Desktop API for precise control** - When you need exact actions 3. **Combine both APIs** - Create tasks via Agent API, monitor via Desktop API 4. **Handle errors gracefully** - Implement retry logic for transient failures 5. **Monitor resource usage** - Both APIs can be resource-intensive ## Next Steps Get your APIs running See the APIs in action ================================================ FILE: docs/api-reference/openapi.json ================================================ { "openapi": "3.1.0", "info": { "title": "OpenAPI Plant Store", "description": "A sample API that uses a plant store as an example to demonstrate features in the OpenAPI specification", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "http://sandbox.mintlify.com" } ], "security": [ { "bearerAuth": [] } ], "paths": { "/plants": { "get": { "description": "Returns all plants from the system that the user has access to", "parameters": [ { "name": "limit", "in": "query", "description": "The maximum number of results to return", "schema": { "type": "integer", "format": "int32" } } ], "responses": { "200": { "description": "Plant response", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Plant" } } } } }, "400": { "description": "Unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "description": "Creates a new plant in the store", "requestBody": { "description": "Plant to add to the store", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewPlant" } } }, "required": true }, "responses": { "200": { "description": "plant response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plant" } } } }, "400": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/plants/{id}": { "delete": { "description": "Deletes a single plant based on the ID supplied", "parameters": [ { "name": "id", "in": "path", "description": "ID of plant to delete", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "204": { "description": "Plant deleted", "content": {} }, "400": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "webhooks": { "/plant/webhook": { "post": { "description": "Information about a new plant added to the store", "requestBody": { "description": "Plant added to the store", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewPlant" } } } }, "responses": { "200": { "description": "Return a 200 status to indicate that the data was received successfully" } } } } }, "components": { "schemas": { "Plant": { "required": [ "name" ], "type": "object", "properties": { "name": { "description": "The name of the plant", "type": "string" }, "tag": { "description": "Tag to specify the type", "type": "string" } } }, "NewPlant": { "allOf": [ { "$ref": "#/components/schemas/Plant" }, { "required": [ "id" ], "type": "object", "properties": { "id": { "description": "Identification number of the plant", "type": "integer", "format": "int64" } } } ] }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ================================================ FILE: docs/core-concepts/agent-system.mdx ================================================ --- title: "Agent System" description: "The AI brain that powers your self-hosted desktop automation" --- ## Overview The Bytebot Agent System transforms a simple desktop container into an intelligent, autonomous computer user. By combining Claude AI with structured task management, it can understand natural language requests and execute complex workflows just like a human would. Bytebot Agent Architecture ## How the AI Agent Works ### The Brain: Multi-Model AI Integration At the heart of Bytebot is a flexible AI integration that supports multiple models. Choose the AI that best fits your needs: **Anthropic Claude** (Default): - Best for complex reasoning and visual understanding - Excellent at following detailed instructions - Superior performance on desktop automation tasks **OpenAI GPT Models**: - Fast and reliable for general automation - Strong code understanding and generation - Cost-effective for routine tasks **Google Gemini**: - Efficient for high-volume tasks - Good balance of speed and capability - Excellent multilingual support The agent with any model: 1. **Understands Context**: Processes your natural language requests with full conversation history 2. **Plans Actions**: Breaks down complex tasks into executable computer actions 3. **Adapts in Real-time**: Adjusts its approach based on what it sees on screen 4. **Learns from Feedback**: Improves task execution through conversation ### Conversation Flow "Research competitors for my SaaS product and create a comparison table" The AI model understands the request and plans: open browser → search → visit sites → extract data → create document The agent controls the desktop: clicking, typing, taking screenshots, reading content Real-time status updates and asks for clarification when needed Completes the task and provides the output (files, screenshots, summaries) ## Task Management System ### Task Lifecycle Tasks move through a structured lifecycle: ```mermaid graph LR A[Created] --> B[Queued] B --> C[Running] C --> D[Needs Help] C --> E[Completed] C --> F[Failed] D --> C ``` ### Task Properties Each task contains: - **Description**: What needs to be done - **Priority**: Urgent, High, Medium, or Low - **Status**: Current state in the lifecycle - **Type**: Immediate or Scheduled - **History**: All messages and actions taken ### Smart Task Processing The agent processes tasks intelligently: 1. **Priority Queue**: Urgent tasks run first 2. **Error Recovery**: Automatically retries failed actions 3. **Human in the Loop**: Asks for help when stuck 4. **Context Preservation**: Maintains conversation history across sessions ## Real-world Capabilities ### What the Agent Can Do - Browse websites - Fill out forms - Extract data - Download files - Monitor changes - Create documents - Edit spreadsheets - Generate reports - Organize files - Convert formats - Access webmail through browser - Read and extract information - Fill contact forms - Navigate communication portals - Handle verification flows - Extract from PDFs - Process CSV files - Create visualizations - Generate summaries - Transform data ## Technical Architecture ### Core Components 1. **NestJS Agent Service** - Integrates with multiple AI provider APIs (Anthropic, OpenAI, Google) - Handles WebSocket connections - Coordinates with desktop API 2. **Message System** - Structured conversation format - Supports text and images - Maintains full context - Enables rich interactions 3. **Database Schema** ```sql Tasks: id, description, status, priority, timestamps Messages: id, task_id, role, content, timestamps Summaries: id, task_id, content, parent_id ``` 4. **Computer Action Bridge** - Translates AI decisions to desktop actions - Handles screenshots and feedback - Manages action timing - Provides error handling ### API Endpoints Key endpoints for programmatic control: ```typescript // Create a new task POST /tasks { "description": "Your task description", "priority": "HIGH", "type": "IMMEDIATE" } // Get task status GET /tasks/:id // Send a message POST /tasks/:id/messages { "content": "Additional instructions" } // Get task history GET /tasks/:id/messages ``` ## Chat UI Features The web interface provides: ### Real-time Interaction - Live chat with the AI agent - Instant status updates - Progress indicators - Error notifications ### Visual Feedback - Embedded desktop viewer - Screenshot history - Action replay - Task timeline ### Task Management - Create and prioritize tasks - View active and completed tasks - Export conversation logs - Manage task queues ## Security & Privacy ### Data Isolation - All processing happens in your infrastructure - No data sent to external services (except your chosen AI provider API) - Conversations stored locally - Complete audit trail ### Access Control - Configurable authentication - API key management - Network isolation options ## Extending the Agent ### Integration Points - External API calls via the Agent API - Custom AI prompts for specialized workflows - MCP protocol support for tool integration ### Best Practices 1. **Clear Instructions**: Be specific about desired outcomes 2. **Break Down Complex Tasks**: Use multiple smaller tasks for better results 3. **Provide Context**: Include relevant files or URLs 4. **Monitor Progress**: Watch the desktop view for real-time feedback 5. **Review Results**: Verify outputs meet requirements ## Troubleshooting - Check your AI provider API key is valid - Verify agent service is running - Review logs for errors - Ensure sufficient API credits/quota with your provider - Monitor system resources - Check network latency - Reduce screenshot frequency - Optimize AI prompts for your chosen model - Consider switching to a faster model (e.g., Gemini Flash) ## Next Steps Get your agent running Integrate with your apps See what's possible Optimize your workflows ================================================ FILE: docs/core-concepts/architecture.mdx ================================================ --- title: "Architecture" description: "How Bytebot's desktop agent works under the hood" --- ## Overview Bytebot is a self-hosted AI desktop agent built with a modular architecture. It combines a Linux desktop environment with AI to create an autonomous computer user that can perform tasks through natural language instructions. Bytebot Architecture Diagram ## System Architecture The system consists of four main components that work together: ### 1. Bytebot Desktop Container The foundation of the system - a virtual Linux desktop that provides: - **Ubuntu 22.04 LTS** base for stability and compatibility - **XFCE4 Desktop** for a lightweight, responsive UI - **bytebotd Daemon** - The automation service built on nutjs that executes computer actions - **Pre-installed Applications**: Firefox ESR, Thunderbird, text editors, and development tools - **noVNC** for remote desktop access **Key Features:** - Runs completely isolated from your host system - Consistent environment across different platforms - Can be customized with additional software - Accessible via REST API on port 9990 - MCP SSE endpoint available at `/mcp` - Uses shared types from `@bytebot/shared` package ### 2. AI Agent Service The brain of the system - orchestrates tasks using an LLM: - **NestJS Framework** for robust, scalable backend - **LLM Integration** supporting Anthropic Claude, OpenAI GPT, and Google Gemini models - **WebSocket Support** for real-time updates - **Computer Use API Client** to control the desktop - **Prisma ORM** for database operations - **Tool definitions** for computer actions (mouse, keyboard, screenshots) **Responsibilities:** - Interprets natural language requests - Plans sequences of computer actions - Manages task state and progress - Handles errors and retries - Provides real-time task updates via WebSocket ### 3. Web Task Interface The user interface for interacting with your AI agent: - **Next.js 15 Application** with TypeScript for type safety - **Embedded VNC Viewer** to watch the desktop in action - **Task Management** UI with status badges - **WebSocket Connections** for live updates - **Reusable components** for consistent UI - **API utilities** for streamlined server communication **Features:** - Task creation and management interface - Desktop tab for direct manual control - Real-time desktop viewer with takeover mode - Task history and status tracking - Responsive design for all devices ### 4. PostgreSQL Database Persistent storage for the agent system: - **Tasks Table**: Stores task details, status, and metadata - **Messages Table**: Stores AI conversation history - **Prisma ORM** for type-safe database access ## Data Flow ### Task Execution Flow User describes a task in natural language via the chat UI Agent service creates a task record and adds it to the processing queue The LLM analyzes the task and generates a plan of computer actions Agent sends computer actions to bytebotd via REST API or MCP bytebotd executes actions (mouse, keyboard, screenshots) on the desktop Agent receives results, updates task status, and continues or completes Results and status updates are sent back to the user in real-time ### Communication Protocols ```mermaid graph LR A[Tasks UI] -->|WebSocket| B[Agent Service] A -->|HTTP Proxy| C[Desktop VNC] B -->|REST/MCP| D[Desktop API] B -->|SQL| E[PostgreSQL] B -->|HTTPS| F[LLM Provider] D -->|IPC| G[bytebotd] ``` ## Security Architecture ### Isolation Layers 1. **Container Isolation** - Each desktop runs in its own Docker container - No access to host filesystem by default - Network isolation with explicit port mapping 2. **Process Isolation** - bytebotd runs as non-root user - Separate processes for different services - Resource limits enforced by Docker 3. **Network Security** - Services only accessible from localhost by default - Can be configured with authentication - HTTPS/WSS for external connections ### API Security - **Desktop API**: No authentication by default (localhost only). Supports REST and MCP. - **Agent API**: Can be secured with API keys - **Database**: Password protected, not exposed externally Default configuration is for development. For production: - Enable authentication on all APIs - Use HTTPS/WSS for all connections - Implement network policies - Rotate credentials regularly ## Deployment Patterns ### Single User (Development) ```yaml Services: All on one machine Scale: 1 instance each Use Case: Personal automation, development Resources: 4GB RAM, 2 CPU cores ``` ### Production Deployment ```yaml Services: All services on dedicated hardware Scale: Single instance (1 agent, 1 desktop) Use Case: Business automation Resources: 8GB+ RAM, 4+ CPU cores ``` ### Enterprise Deployment ```yaml Services: Kubernetes orchestration Scale: Single instance with high availability Use Case: Organization-wide automation Resources: Dedicated nodes ``` ## Extension Points ### Custom Tools Add specialized software to the desktop: ```dockerfile FROM bytebot/desktop:latest RUN apt-get update && apt-get install -y \ your-custom-tools ``` ### AI Integrations Extend agent capabilities: - Custom tools for the LLM - Additional AI models - Specialized prompts - Domain-specific knowledge ## Performance Considerations ### Resource Usage - **Desktop Container**: ~1GB RAM idle, 2GB+ active - **Agent Service**: ~256MB RAM - **UI Service**: ~128MB RAM - **Database**: ~256MB RAM ### Optimization Tips 1. Allocate sufficient resources to containers 2. Limit concurrent tasks to prevent overload 3. Monitor resource usage regularly 4. Use LiteLLM proxy for provider flexibility ## Next Steps Learn about the AI agent capabilities Explore the virtual desktop environment Integrate with your applications Deploy your own instance ================================================ FILE: docs/core-concepts/desktop-environment.mdx ================================================ --- title: "Desktop Environment" description: "The virtual Linux desktop where Bytebot performs tasks" --- ## Overview The Bytebot Desktop Environment (also called Bytebot Core) is a complete Linux desktop that runs in a Docker container. This is where Bytebot does its work - clicking buttons, typing text, browsing websites, and using applications just like you would. Bytebot Desktop Environment ## Why a Virtual Desktop? ### Complete Isolation - **No Risk to Host**: All actions happen inside the container - **Sandboxed Environment**: Desktop can't access your host system - **Easy Reset**: Destroy and recreate in seconds - **Clean Workspace**: Each restart provides a fresh environment ### Consistency Everywhere - **Platform Independent**: Same environment on Mac, Windows, or Linux - **Reproducible**: Identical setup every time - **Version Control**: Pin specific versions for stability - **No Dependencies**: Everything included in the container ### Built for Automation - **Predictable UI**: Consistent element positioning - **Clean Environment**: No popups or distractions - **Automation-Ready**: Optimized for programmatic control - **Fast Startup**: Desktop ready in seconds ## Technical Stack ### Base System - **Ubuntu 22.04 LTS**: Stable, well-supported Linux distribution - **XFCE4 Desktop**: Lightweight, responsive desktop environment - **X11 Display Server**: Standard Linux graphics system - **supervisord**: Service management ### Pre-installed Software - Firefox ESR (Extended Support Release) - Pre-configured for automation - Clean profile without distractions - Text editor - Office tools - PDF viewer - File manager - Thunderbird email client - Terminal emulator - 1Password password manager - Visual Studio Code (VSCode) - Git version control - Python 3 environment ### Core Services 1. **bytebotd Daemon** - Runs on port 9990 - Handles all automation requests - Built on nutjs framework - Provides REST API 2. **noVNC Web Client** - Browser-based desktop access - No client installation needed - WebSocket proxy included 3. **Supervisor** - Process management - Service monitoring - Automatic restarts - Log management ## Desktop Features ### Display Configuration ```bash # Resolution 1920x1080 @ 24-bit color ``` ### User Environment - **Username**: `user` - **Home Directory**: `/home/user` - **Sudo Access**: Yes (passwordless) - **Desktop Session**: Auto-login enabled ### File System ``` /home/user/ ├── Desktop/ # Desktop shortcuts ├── Documents/ # User documents ├── Downloads/ # Browser downloads ├── .config/ # Application configs └── .local/ # User data ``` ## Accessing the Desktop ### Web Browser (Recommended) Navigate to `http://localhost:9990/vnc` for instant access: - No software installation required - Works on any device with a browser - Supports touch devices - Clipboard sharing ### MCP Control The core container also exposes an [MCP](https://github.com/rekog-labs/MCP-Nest) endpoint. Connect your MCP client to `http://localhost:9990/mcp` to invoke these tools over SSE. ```json { "mcpServers": { "bytebot": { "command": "npx", "args": [ "mcp-remote", "http://127.0.0.1:9990/mcp", "--transport", "http-first" ] } } } ``` ### Direct API Control Most efficient for automation: ```bash # Take a screenshot curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "screenshot"}' # Move mouse curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "move_mouse", "coordinate": {"x": 500, "y": 300}}' ``` ## Customization ### Adding Software Create a custom Dockerfile: ```dockerfile FROM ghcr.io/bytebot-ai/bytebot-desktop:edge # Install additional packages RUN apt-get update && apt-get install -y \ slack-desktop \ zoom \ your-custom-app # Copy configuration files COPY configs/ /home/user/.config/ ``` ## Performance Optimization ### Resource Allocation ```yaml # Recommended settings deploy: resources: limits: cpus: '2' memory: 4G reservations: cpus: '1' memory: 2G ``` ## Security Hardening Default configuration prioritizes ease of use. For production, apply these security measures: ### Essential Security Steps 1. **Change Default Passwords** ```bash # Set user password passwd bytebot ``` 2. **Limit Network Access** ```yaml # Whitelist specific domains environment: - ALLOWED_DOMAINS=company.com,trusted-site.com # Or restrict to local network only ports: - "10.0.0.0/8:9990:9990" ``` ## Troubleshooting Check logs: ```bash docker logs bytebot-desktop ``` Common issues: - Insufficient memory - Port conflicts - Display server errors Monitor resources: ```bash docker stats bytebot-desktop ``` Solutions: - Increase memory allocation - Check disk space - Update container image ## Best Practices 1. **Regular Updates**: Keep the base image updated for security patches 2. **Persistent Storage**: Mount volumes for important data 3. **Backup Configurations**: Save customizations outside the container 4. **Monitor Resources**: Track CPU/memory usage 5. **Clean Temporary Files**: Periodic cleanup for performance ## Next Steps Deploy your first agent Control the desktop programmatically Add AI capabilities Set up authentication ================================================ FILE: docs/core-concepts/rpa-comparison.mdx ================================================ --- title: "Bytebot vs Traditional RPA" description: "How Bytebot revolutionizes enterprise automation beyond traditional RPA tools" --- # The Next Generation of Enterprise Automation Bytebot represents a fundamental shift in how businesses approach process automation. While traditional RPA tools like UiPath, Automation Anywhere, and Blue Prism require extensive scripting and brittle workflows, Bytebot leverages AI to understand and execute tasks like a human would. ## Traditional RPA Limitations Traditional RPA breaks when UI elements change even slightly Requires specialized developers and lengthy implementation cycles Constant updates needed as applications evolve Can't handle unexpected scenarios or variations ## How Bytebot is Different ### Visual Intelligence vs Element Mapping **Traditional RPA:** ```xml ``` **Bytebot:** ``` "Click the blue Submit button at the bottom of the form" ``` Bytebot understands interfaces visually, just like a human. It doesn't rely on fragile technical selectors that break with every update. ### Natural Language vs Complex Scripting **Traditional RPA Workflow:** - Design in Studio - Map every element - Script error handling - Test extensively - Deploy with fingers crossed - Fix when it breaks (often) **Bytebot Workflow:** - Describe what you need - Bytebot figures it out - Handles errors intelligently - Adapts to changes automatically ## Real-World Enterprise Examples ### Financial Services Automation ```csharp // 500+ lines of code to handle one banking portal var loginPage = new LoginPageObject(); loginPage.WaitForElement("username", 30); loginPage.EnterText("username", credentials.User); loginPage.EnterText("password", credentials.Pass); // Handle 2FA with complex conditional logic if (loginPage.Has2FAPrompt()) { var method = loginPage.Get2FAMethod(); switch(method) { case "SMS": // 50 more lines of code case "Email": // 50 more lines of code case "Authenticator": // 50 more lines of code } } // Download statements with exact selectors navigation.ClickElement("xpath://div[@id='acct-menu']"); navigation.ClickElement("xpath://a[contains(@href,'statements')]"); // ... continues for hundreds more lines ``` ``` Task: "Log into Chase banking portal, navigate to statements, download all statements from last month for account ending in 4521, and save them to Finance/BankStatements/Chase/" That's it. Bytebot handles everything - including 2FA - automatically. ``` ### Multi-System Integration A FinTech company needed to automate operators who: 1. Log into multiple banking portals with 2FA 2. Download transaction files 3. Run proprietary scripts on those files 4. Upload results to internal systems **Traditional RPA Challenge:** - 6 months to implement - Breaks monthly with UI changes - Requires dedicated maintenance team - Can't handle new banks without development - Complex 2FA handling logic for each bank **Bytebot Solution:** - Deployed in 1 week - Adapts to UI changes automatically - 2FA handled automatically via password manager - New banks added with simple instructions - Zero manual intervention required ## Performance Comparison | Metric | Traditional RPA | Bytebot | |--------|----------------|---------| | **Implementation Time** | 3-6 months | 1-2 weeks | | **Developer Requirement** | RPA specialists | Any technical user | | **Maintenance Effort** | 40% of dev time | Near zero | | **Handling UI Changes** | Breaks immediately | Adapts automatically | | **Error Recovery** | Pre-scripted only | Intelligent adaptation | | **New Process Addition** | Weeks of development | Minutes to describe | | **Cost** | $100k+ annually | Self-hosted on your infrastructure | ## Common RPA Migration Patterns ### 1. Invoice Processing **Before (UiPath):** - 2000+ lines of workflow XML - Breaks when vendor portal updates - Requires exact folder structures - Failed on unexpected popups **After (Bytebot):** - One paragraph description - Handles portal changes - Asks for help when needed - Processes variations intelligently ### 2. Compliance Reporting **Before (Automation Anywhere):** - Complex bot orchestration - Separate bots per system - Rigid scheduling - No flexibility **After (Bytebot):** - Single unified workflow - Natural language instructions - Dynamic adaptation - Human collaboration when needed ### 3. Data Migration **Before (Blue Prism):** - Massive process definitions - Exact field mapping required - Breaks on data variations - Limited error handling **After (Bytebot):** - Describe the mapping rules - Handles variations intelligently - Asks for clarification - Visual validation included ## Integration with Existing RPA Bytebot can work alongside existing RPA investments: ```mermaid graph LR A[Legacy RPA] -->|Handles stable processes| B[Structured Systems] C[Bytebot] -->|Handles complex/changing processes| D[Dynamic Systems] C -->|Takes over when RPA fails| A E[Human Operator] -->|Guides via takeover mode| C ``` ## Enterprise Architecture ### Deployment Options Deploy in your data center for maximum security and compliance Use your AWS/Azure/GCP infrastructure with full control Process sensitive data locally, leverage cloud for scaling Completely isolated deployment for classified environments ### Security & Compliance - **Data Sovereignty**: All processing on your infrastructure - **Audit Trails**: Complete logs of every action - **Access Control**: Integrate with your IAM/SSO - **Compliance**: SOC2, HIPAA, PCI-DSS compatible deployments ## Getting Started with Migration List your current RPA workflows, especially: - Those that break frequently - Require regular maintenance - Handle multiple systems - Need human decision points Pick one problematic workflow: - Document the business process - Deploy Bytebot - Describe the task naturally - Compare results As confidence grows: - Migrate more complex processes - Retire brittle RPA bots - Reduce maintenance overhead - Scale across departments ## Next Steps Deploy Bytebot in your environment View source code and contribute Join our Discord for support Get help with enterprise deployments **Ready to move beyond traditional RPA?** Bytebot brings human-like intelligence to process automation, eliminating the brittleness and complexity of traditional tools while delivering enterprise-grade reliability and security. ================================================ FILE: docs/deployment/helm.mdx ================================================ --- title: "Helm Deployment" description: "Deploy Bytebot on Kubernetes using Helm charts" --- # Deploy Bytebot on Kubernetes with Helm Helm provides a simple way to deploy Bytebot on Kubernetes clusters. ## Prerequisites - Kubernetes cluster (1.19+) - Helm 3.x installed - kubectl configured - 8GB+ available memory in cluster ## Quick Start ```bash git clone https://github.com/bytebot-ai/bytebot.git cd bytebot ``` Create a `values.yaml` file with at least one API key: ```yaml bytebot-agent: apiKeys: anthropic: value: "sk-ant-your-key-here" # Optional: Add more providers # openai: # value: "sk-your-key-here" # gemini: # value: "your-key-here" ``` ```bash helm install bytebot ./helm \ --namespace bytebot \ --create-namespace \ -f values.yaml ``` ```bash # Port-forward for local access kubectl port-forward -n bytebot svc/bytebot-ui 9992:9992 # Access at http://localhost:9992 ``` ## Basic Configuration ### API Keys Configure at least one AI provider: ```yaml bytebot-agent: apiKeys: anthropic: value: "sk-ant-your-key-here" openai: value: "sk-your-key-here" gemini: value: "your-key-here" ``` ### Resource Limits (Optional) Adjust resources based on your needs: ```yaml # Desktop container (where automation runs) desktop: resources: requests: memory: "2Gi" cpu: "1" limits: memory: "4Gi" cpu: "2" # Agent (AI orchestration) agent: resources: requests: memory: "1Gi" cpu: "500m" ``` ### External Access (Optional) Enable ingress for domain-based access: ```yaml ui: ingress: enabled: true hostname: bytebot.your-domain.com tls: true ``` ## Accessing Bytebot ### Local Access (Recommended) ```bash kubectl port-forward -n bytebot svc/bytebot-ui 9992:9992 ``` Access at: http://localhost:9992 ### External Access If you configured ingress: - Access at: https://bytebot.your-domain.com ## Verifying Deployment Check that all pods are running: ```bash kubectl get pods -n bytebot ``` Expected output: ``` NAME READY STATUS RESTARTS AGE bytebot-agent-xxxxx 1/1 Running 0 2m bytebot-desktop-xxxxx 1/1 Running 0 2m bytebot-postgresql-0 1/1 Running 0 2m bytebot-ui-xxxxx 1/1 Running 0 2m ``` ## Troubleshooting ### Pods Not Starting Check pod status: ```bash kubectl describe pod -n bytebot ``` Common issues: - Insufficient memory/CPU: Check node resources with `kubectl top nodes` - Missing API keys: Verify your values.yaml configuration ### Connection Issues Test service connectivity: ```bash kubectl logs -n bytebot deployment/bytebot-agent ``` ### View Logs ```bash # All logs kubectl logs -n bytebot -l app=bytebot --tail=100 # Specific component kubectl logs -n bytebot deployment/bytebot-agent ``` ## Upgrading ```bash # Update your values.yaml as needed, then: helm upgrade bytebot ./helm -n bytebot -f values.yaml ``` ## Uninstalling ```bash # Remove Bytebot helm uninstall bytebot -n bytebot # Clean up namespace kubectl delete namespace bytebot ``` ## Advanced Configuration If using Kubernetes secret management (Vault, Sealed Secrets, etc.): ```yaml bytebot-agent: apiKeys: anthropic: useExisting: true secretName: "my-api-keys" secretKey: "anthropic-key" ``` Create the secret manually: ```bash kubectl create secret generic my-api-keys \ --namespace bytebot \ --from-literal=anthropic-key="sk-ant-your-key" ``` For centralized LLM management, use the included LiteLLM proxy: ```bash helm install bytebot ./helm \ -f values-proxy.yaml \ --namespace bytebot \ --create-namespace \ --set bytebot-llm-proxy.env.ANTHROPIC_API_KEY="your-key" ``` This provides: - Centralized API key management - Request routing and load balancing - Rate limiting and retry logic Configure persistent storage: ```yaml desktop: persistence: enabled: true size: "20Gi" storageClass: "fast-ssd" postgresql: persistence: size: "20Gi" storageClass: "fast-ssd" ``` ```yaml # Network policies networkPolicy: enabled: true # Pod security podSecurityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 # Enable authentication auth: enabled: true type: "basic" username: "admin" password: "changeme" # Use secrets in production! ``` ## Next Steps Integrate Bytebot with your applications Use any LLM provider with Bytebot **Need help?** Join our [Discord community](https://discord.com/invite/d9ewZkWPTP) or check our [GitHub discussions](https://github.com/bytebot-ai/bytebot/discussions). ================================================ FILE: docs/deployment/litellm.mdx ================================================ --- title: "LiteLLM Integration" description: "Use any LLM provider with Bytebot through LiteLLM proxy" --- # Connect Any LLM to Bytebot with LiteLLM LiteLLM acts as a unified proxy that lets you use 100+ LLM providers with Bytebot - including Azure OpenAI, AWS Bedrock, Anthropic, Hugging Face, Ollama, and more. This guide shows you how to set up LiteLLM with Bytebot. ## Why Use LiteLLM? Use Azure, AWS, GCP, Anthropic, OpenAI, Cohere, and local models Monitor spending across all providers in one place Distribute requests across multiple models and providers Automatic failover when primary models are unavailable ## Quick Start with Bytebot's Built-in LiteLLM Proxy Bytebot includes a pre-configured LiteLLM proxy service that makes it easy to use any LLM provider. Here's how to set it up: The easiest way is to use the proxy-enabled Docker Compose file: ```bash # Clone Bytebot git clone https://github.com/bytebot-ai/bytebot.git cd bytebot # Set up your API keys in docker/.env cat > docker/.env << EOF # Add any combination of these keys ANTHROPIC_API_KEY=sk-ant-your-key-here OPENAI_API_KEY=sk-your-key-here GEMINI_API_KEY=your-key-here EOF # Start Bytebot with LiteLLM proxy docker-compose -f docker/docker-compose.proxy.yml up -d ``` This automatically: - Starts the `bytebot-llm-proxy` service on port 4000 - Configures the agent to use the proxy via `BYTEBOT_LLM_PROXY_URL` - Makes all configured models available through the proxy To add custom models or providers, edit the LiteLLM config: ```yaml # packages/bytebot-llm-proxy/litellm-config.yaml model_list: # Add Azure OpenAI - model_name: azure-gpt-4o litellm_params: model: azure/gpt-4o-deployment api_base: https://your-resource.openai.azure.com/ api_key: os.environ/AZURE_API_KEY api_version: "2024-02-15-preview" # Add AWS Bedrock - model_name: claude-bedrock litellm_params: model: bedrock/anthropic.claude-3-5-sonnet aws_region_name: us-east-1 # Add local models via Ollama - model_name: local-llama litellm_params: model: ollama/llama3:70b api_base: http://host.docker.internal:11434 ``` Then rebuild: ```bash docker-compose -f docker/docker-compose.proxy.yml up -d --build ``` The Bytebot agent automatically queries the proxy for available models: ```bash # Check available models through Bytebot API curl http://localhost:9991/tasks/models # Or directly from LiteLLM proxy curl http://localhost:4000/model/info ``` The UI will show all available models in the model selector. ## How It Works ### Architecture ```mermaid graph LR A[Bytebot UI] -->|Select Model| B[Bytebot Agent] B -->|BYTEBOT_LLM_PROXY_URL| C[LiteLLM Proxy :4000] C -->|Route Request| D[Anthropic API] C -->|Route Request| E[OpenAI API] C -->|Route Request| F[Google API] C -->|Route Request| G[Any Provider] ``` ### Key Components 1. **bytebot-llm-proxy Service**: A LiteLLM instance running in Docker that: - Runs on port 4000 within the Bytebot network - Uses the config from `packages/bytebot-llm-proxy/litellm-config.yaml` - Inherits API keys from environment variables 2. **Agent Integration**: The Bytebot agent: - Checks for `BYTEBOT_LLM_PROXY_URL` environment variable - If set, queries the proxy at `/model/info` for available models - Routes all LLM requests through the proxy 3. **Pre-configured Models**: Out of the box support for: - Anthropic: Claude Opus 4, Claude Sonnet 4 - OpenAI: GPT-4.1, GPT-4o - Google: Gemini 2.5 Pro, Gemini 2.5 Flash ## Provider Configurations ### Azure OpenAI ```yaml model_list: - model_name: azure-gpt-4o litellm_params: model: azure/gpt-4o-deployment-name api_base: https://your-resource.openai.azure.com/ api_key: your-azure-key api_version: "2024-02-15-preview" - model_name: azure-gpt-4o-vision litellm_params: model: azure/gpt-4o-deployment-name api_base: https://your-resource.openai.azure.com/ api_key: your-azure-key api_version: "2024-02-15-preview" supports_vision: true ``` ### AWS Bedrock ```yaml model_list: - model_name: claude-bedrock litellm_params: model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0 aws_region_name: us-east-1 # Uses AWS credentials from environment - model_name: llama-bedrock litellm_params: model: bedrock/meta.llama3-70b-instruct-v1:0 aws_region_name: us-east-1 ``` ### Google Vertex AI ```yaml model_list: - model_name: gemini-vertex litellm_params: model: vertex_ai/gemini-1.5-pro vertex_project: your-gcp-project vertex_location: us-central1 # Uses GCP credentials from environment ``` ### Local Models (Ollama) ```yaml model_list: - model_name: local-llama litellm_params: model: ollama/llama3:70b api_base: http://ollama:11434 - model_name: local-mixtral litellm_params: model: ollama/mixtral:8x7b api_base: http://ollama:11434 ``` ### Hugging Face ```yaml model_list: - model_name: hf-llama litellm_params: model: huggingface/meta-llama/Llama-3-70b-chat-hf api_key: hf_your_token ``` ## Advanced Features ### Load Balancing Distribute requests across multiple providers: ```yaml model_list: - model_name: gpt-4o litellm_params: model: gpt-4o api_key: sk-openai-key - model_name: gpt-4o # Same name for load balancing litellm_params: model: azure/gpt-4o api_base: https://azure.openai.azure.com/ api_key: azure-key router_settings: routing_strategy: "least-busy" # or "round-robin", "latency-based" ``` ### Fallback Models Configure automatic failover: ```yaml model_list: - model_name: primary-model litellm_params: model: claude-3-5-sonnet-20241022 api_key: sk-ant-key - model_name: fallback-model litellm_params: model: gpt-4o api_key: sk-openai-key router_settings: model_group_alias: "smart-model": ["primary-model", "fallback-model"] # Use "smart-model" in Bytebot config ``` ### Cost Controls Set spending limits and track usage: ```yaml general_settings: master_key: sk-litellm-master database_url: "postgresql://user:pass@localhost:5432/litellm" # Budget limits max_budget: 100 # $100 monthly limit budget_duration: "30d" # Per-model limits model_max_budget: gpt-4o: 50 claude-3-5-sonnet: 50 litellm_settings: callbacks: ["langfuse"] # For detailed tracking ``` ### Rate Limiting Prevent API overuse: ```yaml model_list: - model_name: rate-limited-gpt litellm_params: model: gpt-4o api_key: sk-key rpm: 100 # Requests per minute tpm: 100000 # Tokens per minute ``` ## Alternative Setup: External LiteLLM Proxy If you prefer to run LiteLLM separately or have an existing LiteLLM deployment: ### Option 1: Modify docker-compose.yml ```yaml # docker-compose.yml (without built-in proxy) services: bytebot-agent: environment: # Point to your external LiteLLM instance - BYTEBOT_LLM_PROXY_URL=http://your-litellm-server:4000 # ... rest of config ``` ### Option 2: Use Environment Variable ```bash # Set the proxy URL before starting export BYTEBOT_LLM_PROXY_URL=http://your-litellm-server:4000 # Start normally docker-compose -f docker/docker-compose.yml up -d ``` ### Option 3: Run Standalone LiteLLM ```bash # Run your own LiteLLM instance docker run -d \ --name litellm-external \ -p 4000:4000 \ -v $(pwd)/custom-config.yaml:/app/config.yaml \ -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ ghcr.io/berriai/litellm:main \ --config /app/config.yaml # Then start Bytebot with: export BYTEBOT_LLM_PROXY_URL=http://localhost:4000 docker-compose up -d ``` ## Kubernetes Setup Deploy with Helm: ```yaml # litellm-values.yaml replicaCount: 2 image: repository: ghcr.io/berriai/litellm tag: main service: type: ClusterIP port: 4000 config: model_list: - model_name: claude-3-5-sonnet litellm_params: model: claude-3-5-sonnet-20241022 api_key: ${ANTHROPIC_API_KEY} general_settings: master_key: ${LITELLM_MASTER_KEY} # Then in Bytebot values.yaml: agent: openai: enabled: true apiKey: "${LITELLM_MASTER_KEY}" baseUrl: "http://litellm:4000/v1" model: "claude-3-5-sonnet" ``` ## Monitoring & Debugging ### LiteLLM Dashboard Access metrics and logs: ```bash # Port forward to dashboard kubectl port-forward svc/litellm 4000:4000 # Access at http://localhost:4000/ui # Login with your master_key ``` ### Debug Requests Enable detailed logging: ```yaml litellm_settings: debug: true detailed_debug: true general_settings: master_key: sk-key store_model_in_db: true # Store request history ``` ### Common Issues Check model name matches exactly: ```bash curl http://localhost:4000/v1/models \ -H "Authorization: Bearer sk-key" ``` Verify master key in both LiteLLM and Bytebot: ```bash # Test LiteLLM curl http://localhost:4000/v1/chat/completions \ -H "Authorization: Bearer sk-key" \ -H "Content-Type: application/json" \ -d '{"model": "your-model", "messages": [{"role": "user", "content": "test"}]}' ``` Check latency per provider: ```yaml router_settings: routing_strategy: "latency-based" enable_pre_call_checks: true ``` ## Best Practices ### Model Selection for Bytebot Choose models with strong vision capabilities for best results: - Claude 3.5 Sonnet (Best overall) - GPT-4o (Good vision + reasoning) - Gemini 1.5 Pro (Large context) - Claude 3.5 Haiku (Fast + cheap) - GPT-4o mini (Good balance) - Gemini 1.5 Flash (Very fast) - LLaVA (Vision support) - Qwen-VL (Vision support) - CogVLM (Vision support) ### Performance Optimization ```yaml # Optimize for Bytebot workloads router_settings: routing_strategy: "latency-based" cooldown_time: 60 # Seconds before retrying failed provider num_retries: 2 request_timeout: 600 # 10 minutes for complex tasks # Cache for repeated requests cache: true cache_params: type: "redis" host: "redis" port: 6379 ttl: 3600 # 1 hour ``` ### Security ```yaml general_settings: master_key: ${LITELLM_MASTER_KEY} # IP allowlist allowed_ips: ["10.0.0.0/8", "172.16.0.0/12"] # Audit logging store_model_in_db: true # Encryption encrypt_keys: true # Headers to forward forward_headers: ["X-Request-ID", "X-User-ID"] ``` ## Next Steps Full list of 100+ providers Official LiteLLM proxy server documentation Complete LiteLLM documentation **Pro tip:** Start with a single provider, then add more as needed. LiteLLM makes it easy to switch or combine models without changing Bytebot configuration. ================================================ FILE: docs/deployment/railway.mdx ================================================ --- title: "Deploying Bytebot on Railway" description: "Comprehensive guide to deploying the full Bytebot stack on Railway using the official 1-click template" --- > **TL;DR –** Click the button below, add your AI API key (Anthropic, OpenAI, or Google), and your personal Bytebot instance will be live in ~2 minutes. [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ) --- ## Why Railway? Railway provides a zero-ops PaaS experience with private networking and per-service logs that perfectly fits Bytebot’s multi-container architecture. The official template wires every service together using the latest container images pushed to the `edge` branch. --- ## What Gets Deployed | Service | Container Image (edge) | Port | Exposed? | Purpose | | ---------------- | -------------------------------------------------------------------- | ---- | -------- | ------------------------------------ | | **bytebot-ui** | `ghcr.io/bytebot-ai/bytebot-ui:edge` | 9992 | **Yes** | Next.js web UI rendered to the world | | **bytebot-agent**| `ghcr.io/bytebot-ai/bytebot-agent:edge` | 9991 | No | Task orchestration & LLM calls | | **bytebot-desktop**| `ghcr.io/bytebot-ai/bytebot-desktop:edge` | 9990 | No | Containerised Ubuntu + XFCE desktop | | **postgres** | `postgres:14-alpine` | 5432 | No | Persistence layer | All internal traffic flows through Railway’s [private networking](https://docs.railway.com/guides/private-networking). Only `bytebot-ui` is assigned a public domain. --- ## Step-by-Step Walk-through Click the **Deploy on Railway** button above or visit [https://railway.com/deploy/bytebot?referralCode=L9lKXQ](https://railway.com/deploy/bytebot?referralCode=L9lKXQ). For the bytebot-agent resource, add your AI API key (choose at least one): - **Anthropic**: Paste into `ANTHROPIC_API_KEY` for Claude models - **OpenAI**: Paste into `OPENAI_API_KEY` for GPT models - **Google**: Paste into `GEMINI_API_KEY` for Gemini models Keep other defaults as is. Press **Deploy**. Railway will pull the pre-built images, create the Postgres database and link all services on a private network. When the build logs show *"bytebot-ui: ready"*, click the generated URL (e.g. `https://bytebot-ui-prod.up.railway.app`). You should see the task interface. Create a task and watch the desktop stream! _Tip: You can tail logs for each service from the Railway dashboard._ The first deploy downloads several container layers – expect ~2 minutes. Subsequent redeploys are much faster. --- ## Private Networking & Security • **Private networking** ensures that the agent, desktop and database can communicate securely without exposing their ports to the internet. • **Public exposure** is limited to the UI which serves static assets and proxies WebSocket traffic. • **Add authentication** by placing the UI behind Railway’s built-in password protection or an external provider (e.g. Cloudflare Access, Auth0, OAuth proxy). • You can also point a custom domain to the UI from the Railway dashboard and enable Cloudflare for WAF/CDN protection. --- ## Customisation & Scaling 1. **Change images** – Fork the repo, push your own images and edit the template’s `Dockerfile` references. 2. **Increase resources** – Each service has an independent CPU/RAM slider in Railway. Bump up the desktop or agent if you plan heavy automations. --- ## Troubleshooting | Symptom | Likely Cause | Fix | | ------- | ------------ | ---- | | Web UI shows “connecting…” | Desktop not ready or private networking mis-config | Wait for `bytebot-desktop` container to finish starting, or restart service | | Agent errors `401` or `403` | Missing/invalid API key | Re-enter your AI provider's API key in Railway variables | | Slow desktop video | Free Railway plan throttling | Upgrade plan or reduce screen resolution in desktop settings | --- ## Next Steps • Explore the [REST APIs](/api-reference/introduction) to script tasks programmatically. • Join our [Discord](https://discord.com/invite/d9ewZkWPTP) community for support and showcase your automations! ================================================ FILE: docs/docs.json ================================================ { "$schema": "https://mintlify.com/docs.json", "theme": "mint", "name": "Bytebot - Self-Hosted AI Desktop Agent", "colors": { "primary": "#000000", "light": "#fbfaf9", "dark": "#000000" }, "favicon": "/favicon.svg", "navigation": { "tabs": [ { "tab": "Documentation", "groups": [ { "group": "Getting Started", "pages": ["introduction", "quickstart"] }, { "group": "User Guides", "pages": [ "guides/task-creation", "guides/password-management", "guides/takeover-mode" ] }, { "group": "Deployment", "pages": [ "deployment/railway", "deployment/helm", "deployment/litellm" ] }, { "group": "Core Concepts", "pages": [ "core-concepts/architecture", "core-concepts/agent-system", "core-concepts/desktop-environment", "core-concepts/rpa-comparison" ] } ] }, { "tab": "API Reference", "groups": [ { "group": "Overview", "pages": ["api-reference/introduction"] }, { "group": "Agent API", "pages": [ "api-reference/agent/tasks", "api-reference/agent/ui" ] }, { "group": "Computer Control API", "pages": [ "api-reference/computer-use/unified-endpoint", "api-reference/computer-use/examples" ] } ] } ], "global": { "anchors": [ { "anchor": "GitHub", "href": "https://github.com/bytebot-ai/bytebot", "icon": "github" }, { "anchor": "Discord", "href": "https://discord.gg/zcb5wA2t4u", "icon": "discord" }, { "anchor": "Twitter", "href": "https://x.com/bytebot_ai", "icon": "twitter" }, { "anchor": "Blog", "href": "https://bytebot.ai/blog", "icon": "newspaper" } ] } }, "logo": { "light": "/logo/bytebot_transparent_logo_dark.svg", "dark": "/logo/bytebot_transparent_logo_white.svg" }, "navbar": { "links": [ { "label": "Support", "href": "https://discord.gg/zcb5wA2t4u" } ], "primary": { "type": "button", "label": "Get Started", "href": "https://github.com/bytebot-ai/bytebot" } }, "footer": { "socials": { "github": "https://github.com/bytebot-ai/bytebot", "twitter": "https://twitter.com/bytebotai", "discord": "https://discord.gg/zcb5wA2t4u" } }, "metadata": { "og:title": "Bytebot - Self-Hosted AI Desktop Agent", "og:description": "Automate any computer task with natural language using your own AI desktop agent", "og:image": "/images/agent-architecture.png", "twitter:card": "summary_large_image" } } ================================================ FILE: docs/guides/password-management.mdx ================================================ --- title: "Password Management & 2FA" description: "How Bytebot handles authentication automatically using password managers" --- # Automated Authentication with Bytebot Bytebot can handle authentication automatically - including passwords, 2FA, and even complex multi-step authentication flows - when you set up a password manager extension. **Important**: Password manager extensions are not enabled by default. You need to install them manually using the desktop view. ## How It Works Bytebot comes with 1Password built-in and supports any browser-based password manager extension. It can: - Automatically fill passwords from the password manager - Handle 2FA codes (TOTP/authenticator apps) - Manage multiple accounts across different systems - Work with SSO and federated authentication - Store and use API keys and tokens ## Setting Up Password Management ### Option 1: 1Password (Recommended) 1. Go to the Desktop tab in Bytebot UI 2. Open Firefox 3. Install the 1Password extension from the Firefox Add-ons store 4. Sign in to your 1Password account (or create a dedicated one for Bytebot) In your 1Password admin panel: 1. Create a vault called "Bytebot Automation" 2. Add the credentials Bytebot needs 3. Share the vault with Bytebot's account 4. Set appropriate permissions (read-only recommended) The 1Password extension will automatically: - Detect login forms - Fill credentials - Handle 2FA codes - Submit forms ### Option 2: Other Password Managers You can use any browser-based password manager by installing it through the Desktop view: 1. Open Desktop tab 2. Launch Firefox 3. Install Bitwarden extension from Firefox Add-ons 4. Log in to your Bitwarden account 5. Configure auto-fill settings in Bitwarden preferences 1. Open Desktop tab 2. Launch Firefox 3. Install LastPass extension from Firefox Add-ons 4. Log in with your enterprise account 5. Accept any shared folders for automation credentials 1. Open Desktop tab 2. Install KeePassXC application if needed 3. Install KeePassXC browser extension in Firefox 4. Configure browser integration 5. Load your KeePass database ## Handling Different Authentication Types ### Standard Username/Password ```yaml # Task description Task: "Log into our CRM system and export the customer list" # Bytebot automatically: 1. Navigates to login page 2. Password manager detects form 3. Auto-fills credentials 4. Submits login 5. Proceeds with task ``` ### Time-based 2FA (TOTP) ```yaml # Task description Task: "Access the banking portal and download statements" # Bytebot handles: 1. Enters username/password from password manager 2. When 2FA prompt appears 3. Password manager provides TOTP code 4. Enters code automatically 5. Completes authentication ``` ### Complex Multi-Step Auth ```yaml # Task description Task: "Log into the government portal (uses email verification)" # Bytebot can: 1. Fill initial credentials 2. Handle "send code to email" flows 3. Access webmail account (also in password manager) 4. Retrieve verification code from webmail 5. Complete authentication ``` ## Enterprise Setup Guide ### Centralized Credential Management Set up dedicated service accounts for Bytebot: ``` - bytebot-finance@company.com (banking portals) - bytebot-hr@company.com (HR systems) - bytebot-ops@company.com (operational tools) ``` Structure your password manager: ``` Bytebot Vaults/ ├── Financial Systems/ │ ├── Banking Portal A │ ├── Banking Portal B │ └── Payment Processor ├── Internal Tools/ │ ├── ERP System │ ├── CRM Platform │ └── HR Portal └── External Services/ ├── Vendor Portal 1 ├── Government Site └── Partner System ``` Configure automatic password rotation: ```javascript // Example automation for password rotation { "schedule": "monthly", "task": "For each credential in 'Rotation Required' vault, update password in the system and save new password" } ``` ### Security Best Practices Only share credentials Bytebot needs for specific tasks Enable password manager audit logs to track access Separate vaults by sensitivity level and department Audit Bytebot's credential access monthly ## Common Authentication Scenarios ### Banking and Financial Systems ```yaml Scenario: Daily bank reconciliation across 5 banks Setup: - Each bank credential in password manager - 2FA seeds stored for TOTP generation - Bytebot's IP whitelisted at banks Task: "Log into each bank account, download yesterday's transactions, and consolidate into daily report" Result: Fully automated, no human intervention needed ``` ### Government and Compliance Portals ```yaml Scenario: Weekly regulatory filings Setup: - Service account with 2FA enabled - Password manager has TOTP seed - Security questions stored as notes Task: "Log into state tax portal, file weekly sales tax report using data from tax_data.csv" Handles: Password, 2FA, security questions, CAPTCHAs ``` ### Multi-Tenant SaaS Platforms ```yaml Scenario: Managing multiple client accounts Setup: - Credentials for each tenant/client - Organized in password manager by client - Naming convention: client-platform-role Task: "For each client in client_list.txt, log into their Shopify account and export this month's orders" Scales: Handles 100+ accounts seamlessly ``` ## Advanced Authentication Features ### SSO and SAML Integration ```yaml # Bytebot can handle SSO flows Task: "Log into Salesforce using Okta SSO" Process: 1. Navigate to Salesforce 2. Click "Log in with SSO" 3. Redirect to Okta 4. Password manager fills Okta credentials 5. Handle any 2FA on Okta 6. Redirect back to Salesforce 7. Continue with task ``` ### API Key Management ```yaml # Store API keys in password manager Password Entry: "OpenAI API Key" - Username: "api" - Password: "sk-proj-..." - Notes: "Rate limit: 10000/day" # Use in tasks Task: "Configure the application to use our OpenAI API key from the password manager" ``` ### Certificate-Based Auth ```yaml # For systems requiring certificates Setup: 1. Store certificate password in manager 2. Mount certificate file to Bytebot 3. Configure browser to use certificate Task: "Access the enterprise portal that requires client certificate authentication" ``` ## Troubleshooting Authentication **Solutions:** - Ensure extension is installed and logged in - Check site is saved in password manager - Verify auto-fill settings are enabled - Try refreshing the page **Common causes:** - Time sync issues (check system clock) - Wrong TOTP seed saved - Site using non-standard 2FA **Fix:** ```bash # Sync system time docker exec bytebot-desktop ntpdate -s time.nist.gov ``` **Solutions:** - Enable "remember me" if available - Increase session timeout in target system - Break long tasks into smaller chunks - Use API access where possible ## Integration Examples ### Finance Automation Script ```python # Example: Automated invoice collection tasks = [ { "description": "Log into vendor portal A and download all pending invoices", "credentials": "vault://Financial Systems/Vendor Portal A" }, { "description": "Log into vendor portal B and download all pending invoices", "credentials": "vault://Financial Systems/Vendor Portal B" }, { "description": "Process all downloaded invoices through our AP system", "credentials": "vault://Internal Tools/AP System" } ] # Bytebot handles all authentication automatically ``` ### Compliance Automation ```yaml Daily Compliance Check: Morning: - Log into regulatory portal (2FA enabled) - Download new compliance updates - Check our status If Non-Compliant: - Log into internal system - Create compliance ticket - Notify compliance team All credentials managed automatically ``` ## Best Practices Summary ✅ **DO:** - Use dedicated service accounts for Bytebot - Organize credentials in logical vaults - Enable 2FA on all accounts (Bytebot handles it!) - Rotate passwords regularly - Monitor access logs ❌ **DON'T:** - Share personal credentials with Bytebot - Store passwords in task descriptions - Disable 2FA for convenience - Use the same password across systems - Ignore authentication errors ## Next Steps See auth in action Programmatic credential management **Game Changer**: With proper password manager setup, Bytebot can handle even the most complex authentication flows automatically. No more manual intervention for 2FA, no more sharing passwords insecurely, and no more authentication bottlenecks in your automation workflows! ================================================ FILE: docs/guides/takeover-mode.mdx ================================================ --- title: "Takeover Mode" description: "Take control of the desktop when you need to guide or assist Bytebot" --- # Takeover Mode: Human-AI Collaboration Takeover mode lets you take control of the desktop to help Bytebot when needed. There are two ways to use it: ## 1. During Task Execution In the task detail view, you can hit the takeover button to: - Interrupt the agent if it's going down the wrong path - Guide it towards the correct solution - Resolve issues when it's stumbling on something ## 2. Automatic Activation Takeover mode is automatically enabled when a task status is set to "needs help" - this happens when the agent realizes it can't accomplish something on its own. ## How Actions Are Recorded All your actions during takeover (clicks, drags, scrolls, typing, key presses) are automatically logged in the same unified action space that the agent uses. This means Bytebot understands and learns from everything you do. ## Desktop Tab for Setup Outside of tasks, there's a dedicated **Desktop** tab on the main page that provides: - Free-ranging access to the desktop - Nothing is recorded in this mode - Perfect for: - Installing programs - Logging into apps or websites - Setting up the desktop environment - General desktop maintenance ## Activating Takeover Mode ### Method 1: Manual Takeover During Tasks While Bytebot is working on a task, click on the task to open the detail view. Hit the takeover button to interrupt the agent and take control. Perform the necessary actions to get past the obstacle or show the correct path. Click to release control and let Bytebot continue from where you left off. ### Method 2: Automatic When Help Needed When Bytebot sets a task status to "needs help": - Takeover mode is automatically enabled - You'll see a notification that Bytebot needs assistance - Take control to help resolve the issue - Bytebot will continue once you release control ## Common Use Cases ### 1. Complex UI Navigation **Scenario**: Working with proprietary or complex software **Steps**: 1. Let Bytebot open the application 2. Take control to navigate complex interfaces 3. Use the chat to explain what you're doing 4. Return control for Bytebot to continue **Example**: "Open our internal CRM, I'll show you how to navigate to the reports section" ### 2. Error Recovery **Scenario**: Bytebot encounters an error or gets stuck **Steps**: 1. Notice Bytebot is struggling 2. Take control to resolve the issue 3. Guide it past the problem 4. Explain what went wrong in chat 5. Return control to let Bytebot continue **Example**: "Let me handle this unexpected popup that's blocking the workflow" ### 3. Teaching by Demonstration **Scenario**: Complex multi-step processes **Steps**: 1. Take control when you need to demonstrate 2. Perform the task normally (no need to move slowly) 3. Use chat to explain what you're clicking and why 4. Return control 5. Ask Bytebot to repeat the process **Example**: "Watch me navigate through our vendor portal to find the invoice section" **Important**: Screenshots are taken for every action during takeover mode. Do not enter any data that you don't want captured in screenshots. ## Best Practices ### Do's ✅ - **Use Chat While Taking Over**: Type messages explaining what you're doing and why - **Explain Your Clicks**: Share context about UI elements and their purpose - **Return Control Before Leaving**: Always release control before exiting the task detail view - **Test Understanding**: Ask Bytebot to summarize what it learned ### Don'ts ❌ - **Enter Data You Don't Want Captured**: Screenshots are taken of all actions - **Skip Chat Explanations**: Context helps Bytebot learn patterns - **Leave Task View While in Control**: This will leave the task stuck in takeover mode - **Assume Knowledge**: Explain application-specific workflows **No Need to Move Slowly**: Bytebot captures the state before and after each action, so you can work at normal speed. ## Summary Takeover mode provides flexibility when you need to guide Bytebot or handle situations it can't manage alone. Whether you're navigating complex interfaces, recovering from errors, or teaching new workflows, takeover mode ensures you're always in control when needed. ================================================ FILE: docs/guides/task-creation.mdx ================================================ --- title: "Task Creation & Management" description: "Master the art of creating effective tasks and managing them through completion" --- # Creating and Managing Tasks in Bytebot This guide will walk you through everything you need to know about creating tasks that Bytebot can execute effectively, and managing them through their lifecycle. ## Understanding Tasks A task is any job you want Bytebot to complete. Tasks can be: - **Simple**: "Log in to GitHub" or "Visit example.com" (uses one program) - **Complex**: "Download invoices from email and save them to a folder" (uses multiple programs) - **File-based**: "Read the uploaded PDF and extract all email addresses" (processes uploaded files) - **Collaborative**: "Process invoices, ask me to handle special approvals" ## Working with Files Bytebot has powerful file handling capabilities that make it perfect for document processing and data analysis tasks. ### Uploading Files with Tasks When creating a task, you can upload files that will be automatically saved to the desktop instance. This is incredibly useful for: - **Document Processing**: Upload PDFs, spreadsheets, or documents for Bytebot to analyze - **Data Analysis**: Provide CSV files or datasets for processing - **Template Filling**: Upload forms or templates that need to be completed - **Batch Operations**: Upload multiple files for bulk processing **Game Changer**: Bytebot can read entire files, including PDFs, directly into the LLM context. This means it can process large amounts of data quickly and understand complex documents without manual extraction. ### File Upload Examples 1. Click the attachment button when creating a task 2. Select files to upload (PDFs, CSVs, images, etc.) 3. Files are automatically saved to the desktop 4. Reference them in your task description: ``` "Read the uploaded contracts.pdf and extract all payment terms, then create a summary spreadsheet with vendor names and terms" ``` ```bash # Upload files with task creation (multipart/form-data) curl -X POST http://localhost:9991/tasks \ -F "description=Analyze the uploaded financial statements and create a summary" \ -F "priority=HIGH" \ -F "files=@financial_statements_2024.pdf" \ -F "files=@budget_comparison.xlsx" ``` ### File Processing Capabilities - Extract text from PDFs - Read entire PDFs into context - Parse forms and contracts - Extract tables and data - Read Excel/CSV files - Analyze data patterns - Generate reports - Cross-reference multiple sheets - Summarize long documents - Extract key information - Compare multiple files - Answer questions about content - Process multiple files - Apply same analysis to each - Consolidate results - Generate unified reports ## Creating Your First Task ### Using the Web UI Navigate to `http://localhost:9992` In the input field on the left side, type what you want done. For example: ``` Log in to my GitHub account and check for new notifications ``` Press the arrow button or hit Enter. Bytebot will start loading and begin working on your task. ### Using the API ```bash curl -X POST http://localhost:9991/tasks \ -H "Content-Type: application/json" \ -d '{ "description": "Download all PDF invoices from my email and organize by date", "priority": "HIGH", "type": "IMMEDIATE" }' ``` ## Writing Effective Task Descriptions ### The Golden Rules ❌ "Do some research" ✅ "Research top 5 CRM tools for small businesses" ❌ "Fill out the form" ✅ "Fill out the contact form on example.com with test data" ❌ "Organize files" ✅ "Organize files in Downloads folder by type into subfolders" ❌ "Do multiple unrelated things" ✅ "Focus on a single objective with clear steps" ### Task Description Templates #### Enterprise Process Automation ``` Log into [system] and: 1. [Navigate to specific section] 2. [Download/Extract data] 3. [Process through other system] 4. [Update records/Generate report] Handle any [specific scenarios] Example: Log into our banking portal and: 1. Navigate to wire transfers section 2. Download all pending wire confirmations 3. Match against our ERP payment records 4. Flag any discrepancies in the reconciliation sheet (Bytebot handles all authentication including 2FA automatically via password manager) ``` #### Multi-Application Workflow ``` Access [System A] to get [data] Then in [System B]: 1. [Process the data] 2. [Update records] Finally in [System C]: 1. [Verify updates] 2. [Generate confirmation] Example: Access Salesforce to get list of new customers from today Then in NetSuite: 1. Create customer records with billing info 2. Set up payment terms Finally in our shipping system: 1. Verify addresses are valid 2. Generate welcome kit shipping labels ``` #### Compliance & Audit Task ``` For each [entity] in [source]: 1. Check [compliance requirement] 2. Document [specific data] 3. Flag any [violations/issues] Generate report showing [metrics] Example: For each vendor in our approved vendor list: 1. Check their insurance certificates are current 2. Document expiration dates and coverage amounts 3. Flag any expiring within 30 days Generate report showing compliance percentage by category ``` ## Managing Active Tasks ### Task States Task Lifecycle Tasks move through these states: 1. **Created** → Task is defined but not started 2. **Queued** → Waiting for agent availability 3. **Running** → Actively being worked on 4. **Needs Help** → Requires human input 5. **Completed** → Successfully finished 6. **Failed** → Could not be completed ### Monitoring Progress #### Real-time Updates Watch Bytebot work through the task detail viewer: - **Green dot**: Task is actively running - **Status messages**: Current step being executed - **Desktop view**: See what Bytebot sees in real-time #### Chat Messages Bytebot provides updates like: ``` Assistant: I'm now searching for project management tools... Assistant: Found 15 options, filtering by your criteria... Assistant: Creating the comparison table with 5 tools... ``` ### Interacting with Running Tasks #### Providing Additional Information ``` User: "Also include free tier options in your research" Assistant: "I'll add a column for free tier availability to the comparison table." ``` #### Clarifying Instructions ``` Assistant: "I found multiple forms on this page. Which one should I fill out?" User: "Use the 'Contact Sales' form on the right side" ``` #### Modifying Tasks ``` User: "Actually, make it top 10 tools instead of top 5" Assistant: "I'll expand my research to include 10 tools in the comparison." ``` ## Advanced Task Management ### Task Dependencies Chain tasks that depend on each other: ``` Task 1: "Download the invoice from the vendor portal" Task 2: "Open the downloaded invoice and extract the total amount" Task 3: "Enter the amount into our accounting system" ``` ## Best Practices ### Do's ✅ 1. **Start Simple**: Test with basic tasks before complex ones 2. **Provide Examples**: "Format it like the report from last week" 3. **Include Credentials Safely**: Use takeover mode for passwords 4. **Set Realistic Expectations**: Complex tasks take time 5. **Review Results**: Always verify important outputs ### Don'ts ❌ 1. **Overload Single Tasks**: Break complex workflows into steps 2. **Assume Knowledge**: Explain custom applications 3. **Skip Context**: Always provide necessary background 4. **Ignore Errors**: Address issues promptly 5. **Rush Critical Tasks**: Allow time for careful execution ## Task Examples by Category ### 📄 Document Processing & Analysis ``` "Read the uploaded contract.pdf and extract all key terms including payment schedules, deliverables, and termination clauses. Create a summary document with these details." "Process all the uploaded invoice PDFs, extract vendor names, amounts, and due dates, then create a consolidated Excel spreadsheet sorted by due date." "Analyze the uploaded financial_report.pdf and answer these questions: What was the revenue growth? What are the main risk factors mentioned? What is the debt-to-equity ratio?" "Read through the uploaded employee_handbook.pdf and create a checklist of all compliance requirements mentioned in the document." ``` ### 🏦 Enterprise Automation (RPA-Style Workflows) ``` "Log into our banking portal, download all transaction files from last month, save them to the Finance/Statements folder, then run the reconciliation script on each file." (Note: Bytebot handles all authentication including 2FA automatically using the built-in password manager) "Access the vendor portal at supplier.example.com, navigate to the invoice section, download all pending invoices, extract the data into our standard template, and upload to the AP system." "Open our legacy ERP system, export the customer list, then for each customer, look them up in the new CRM and update their status and last contact date." ``` ### 📊 Financial Operations & Data Analysis ``` "Read the uploaded bank_statements folder containing 12 monthly PDFs, extract all transactions over $10,000, and create a summary report showing patterns and anomalies." "Log into each of our 5 bank accounts, download the daily statements, consolidate them into a single cash position report, and save to the shared finance folder." "Process the uploaded expense_reports.zip file, review all reports over $1,000, create a summary with policy violations flagged, and prepare for approval." "Navigate to the tax authority website, download all GST/VAT returns for Q4, extract the figures, and populate our tax reconciliation spreadsheet." ``` ### 🔄 Multi-System Integration ``` "Pull today's orders from Shopify, create corresponding entries in NetSuite, update inventory in our WMS, and trigger shipping labels in ShipStation." "Extract employee data from Workday, cross-reference with our access control system, identify discrepancies, and create tickets for IT to resolve." "Log into our insurance portal, download policy documents for all active policies, extract key dates and coverage amounts, update our risk management database." ``` ### 📈 Compliance & Reporting ``` "Access all state regulatory websites for our operating regions, check for new compliance updates since last month, download relevant documents, and create a summary report." "Log into our various SaaS tools (list provided), export user access reports, consolidate into a single audit trail, and flag any terminated employees still with access." "Navigate to customer portal, download all SLA performance reports, extract metrics, compare against our internal data, and highlight discrepancies." ``` ### 🤝 Development & QA Integration ``` "After the code agent deploys the new feature, test the complete user journey from signup to checkout, take screenshots at each step, and verify against the design specs." "Run through all test scenarios in our QA checklist, but for any failures, have the code agent analyze the error and attempt a fix, then retest automatically." "Monitor our staging environment, when a new build is deployed, automatically run the smoke test suite and create a visual regression report comparing to production." ``` ## Troubleshooting Common Issues **Possible causes**: - Waiting for slow page/app to load - Encountered unexpected popup - Unclear next step **Solutions**: - Check desktop viewer for current state - Provide clarification via chat - Use takeover mode to help - Cancel and restart with clearer instructions **Possible causes**: - Ambiguous instructions - Website/app changed - Misunderstood context **Solutions**: - Review task description for clarity - Provide specific examples - Break into smaller subtasks - Use takeover mode to demonstrate **Possible causes**: - Invalid URL or application - Missing prerequisites - System resource issues **Solutions**: - Verify URLs and application names - Ensure required files/data exist - Check system resources - Review error messages in chat ## Task Management Tips ### Organizing Multiple Tasks 1. **Use Clear Naming**: Include date, category, or project 2. **Group Related Tasks**: Process similar tasks together 3. **Priority Management**: Reserve 'Urgent' for true emergencies 4. **Regular Reviews**: Check completed tasks for quality ### Performance Optimization - **Batch Similar Tasks**: Group web research, data entry, etc. - **Prepare Resources**: Have files/data ready before starting - **Clear Desktop**: Minimize distractions and popups - **Stable Environment**: Ensure good internet and system resources ### Learning from Tasks After each task: 1. Review the approach Bytebot took 2. Note any inefficiencies 3. Refine future task descriptions 4. Build a library of effective prompts ## Next Steps Learn human-AI collaboration Automate task creation **Pro Tip**: Start with simple tasks to understand Bytebot's capabilities, then gradually increase complexity as you learn what works best. ================================================ FILE: docs/introduction.mdx ================================================ --- title: Introduction description: "Open source AI desktop agent that automates any computer task" ---

Bytebot Logo Bytebot Logo

## What is Bytebot? Bytebot is an open-source AI agent that can control a computer desktop to complete tasks for you. It runs in Docker containers on your own infrastructure, giving you a virtual assistant that can: - Use any desktop application (browser, email, office tools, etc.) - Process uploaded files including PDFs, spreadsheets, and documents - Read entire files directly into the LLM context for rapid analysis - Automate repetitive tasks like data entry and form filling - Handle complex workflows that span multiple applications - Work 24/7 without human supervision Simply describe what you need done in plain English, and Bytebot will figure out how to do it – clicking buttons, typing text, navigating websites, reading documents, and completing tasks just like a human would. ## Why Bytebot Over Traditional RPA? Unlike UiPath or similar tools, no need to design flowcharts or write scripts - just describe tasks naturally AI-powered understanding means Bytebot adapts to UI changes without breaking Can read and understand any interface, not just pre-mapped elements Handles unexpected popups, errors, and variations automatically ## Why Self-Host Bytebot? Your tasks and data never leave your infrastructure. Everything runs locally on your servers. Customize the desktop environment, install any applications, and configure to your exact needs. Use your own LLM API keys without platform restrictions or additional fees. Each desktop runs in its own container, completely isolated from your host system. ## Real-World Use Cases ### Enterprise Automation (RPA Replacement) Bytebot is the next generation of RPA (Robotic Process Automation). It handles the same complex workflows as traditional tools like UiPath, but with AI-powered adaptability and automatic authentication: - **Financial Operations**: Automate banking portal access (including 2FA when password manager extensions are configured), download transaction files, and process them through multiple systems - **Compliance Workflows**: Navigate government websites, download regulatory documents, extract data, and update compliance tracking systems - **Multi-System Integration**: Bridge legacy systems that lack APIs by automating the UI interactions between them - **Vendor Management**: Log into supplier portals, download invoices, reconcile with internal systems, and process payments ### Business Process Automation - **Data Reconciliation**: Pull reports from multiple SaaS platforms, cross-reference data, and generate consolidated reports - **Customer Onboarding**: Navigate between CRM, banking, and verification systems to complete new customer setup - **Purchase Order Processing**: Extract POs from webmail portals, enter into ERP systems, and update inventory databases - **HR Operations**: Collect employee data from various systems, update records, and ensure consistency across platforms ### Development & QA Integration Bytebot becomes even more powerful when combined with coding agents: - **Full-Stack Testing**: Use a coding agent to generate code, then have Bytebot visually test and validate the output - **Automated Debugging**: Let Bytebot reproduce user-reported issues while a coding agent analyzes and fixes the code - **End-to-End Development**: Code agents write features, Bytebot tests them, creating a complete development loop - **Visual Regression Testing**: Automatically detect UI changes across deployments with screenshot comparisons ## How It Works Simply tell Bytebot what you want done in natural language through the tasks interface Bytebot understands your request and breaks it down into specific computer actions Bytebot executes the task on its virtual desktop using the keyboard and mouse Monitor it working in real-time through the task detail view, or let it complete tasks independently. Receive the completed task output, screenshots, or confirmation of completion ## Architecture Overview Bytebot consists of four integrated components working together: Bytebot Agent Architecture Ubuntu 22.04 with XFCE4, VSCode, Firefox, Thunderbird email client, and automation daemon (bytebotd) NestJS service that uses LLMs (Anthropic Claude, OpenAI GPT, Google Gemini) to plan and execute tasks Next.js web app for creating and managing tasks Programmatic access to both task management and direct desktop control ## Getting Started Get Bytebot running in 2 minutes Understand how it all fits together Integrate with your applications ## Key Features ### 🤖 Natural Language Control Just tell Bytebot what you need done. No coding or complex automation tools required. ### 🖥️ Full Desktop Access Bytebot can use any application you can install - browsers, office tools, custom software. ### 🔒 Complete Privacy Runs entirely on your infrastructure. Your data never leaves your servers. ### 🔄 Two Operating Modes - **Autonomous Mode**: Bytebot completes tasks independently - **Takeover Mode**: You can step in and take control when needed ### 🖱️ Direct Desktop Access - **Desktop Tab**: Free-form access to the virtual desktop for setup, installing programs, or manual operations - **Task View**: Watch and interact with Bytebot during task execution ### 🚀 Easy Deployment - One-click deployment on Railway - Docker Compose for self-hosting - Helm charts for Kubernetes ### 🔌 Developer-Friendly - REST APIs for programmatic control - Task management API - Extensible architecture - MCP (Model Context Protocol) support ## Community & Support Join our community for help, tips, and discussions Report issues, contribute, or star the project **Ready to give your AI its own computer?** Start with our [Quick Start Guide](/quickstart) to have your own AI desktop agent running in minutes. ================================================ FILE: docs/quickstart.mdx ================================================ --- title: "Quick Start" description: "Get your AI desktop agent running in 2 minutes" --- # Choose Your Deployment Method Bytebot can be deployed in several ways depending on your needs: ## ☁️ One-click Deploy on Railway [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ) Click the Deploy Now button in the Bytebot template on Railway. Enter either your `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` for the bytebot-agent resource. Hit **Deploy**. Railway will build the stack, wire the services together via private networking and output a public URL for the UI. Your agent should be ready within a couple of minutes! Need more details? See the full Railway deployment guide. ## 🐳 Self-host with Docker Compose ## Prerequisites - Docker ≥ 20.10 - Docker Compose - 4GB+ RAM available - AI API key from one of these providers: - Anthropic ([get one here](https://console.anthropic.com)) - Claude models - OpenAI ([get one here](https://platform.openai.com/api-keys)) - GPT models - Google ([get one here](https://makersuite.google.com/app/apikey)) - Gemini models ## 🚀 2-Minute Setup Get your self-hosted AI desktop agent running with just three commands: ```bash git clone https://github.com/bytebot-ai/bytebot.git cd bytebot # Configure your AI provider (choose one): echo "ANTHROPIC_API_KEY=your_api_key_here" > docker/.env # For Claude # echo "OPENAI_API_KEY=your_api_key_here" > docker/.env # For OpenAI # echo "GEMINI_API_KEY=your_api_key_here" > docker/.env # For Gemini ``` ```bash docker-compose -f docker/docker-compose.yml up -d ``` This starts all four services: - **Bytebot Desktop**: Containerized Linux environment - **AI Agent**: LLM-powered task processor (supports Claude, GPT, or Gemini) - **Chat UI**: Web interface for interaction - **Database**: PostgreSQL for persistence Navigate to [http://localhost:9992](http://localhost:9992) to access the Bytebot UI. **Two ways to interact:** 1. **Tasks**: Enter task descriptions to have Bytebot work autonomously 2. **Desktop**: Direct access to the virtual desktop for manual control Try asking: - "Open Firefox and search for the weather forecast" - "Take a screenshot of the desktop" - "Create a text file with today's date" **First time?** The initial startup may take 2-3 minutes as Docker downloads the images. Subsequent starts will be much faster. ## 🎯 What You Just Deployed You now have a complete AI desktop automation system with: **🔐 Password Manager Support**: Bytebot can handle authentication automatically when you install a password manager extension. See our [password management guide](/guides/password-management) for setup instructions. - Understands natural language - Plans and executes tasks - Adapts to errors - Works autonomously - Full Ubuntu environment - Browser, office tools - File system access - Application support - Create and manage tasks - Real-time desktop view - Conversation history - Takeover mode - Programmatic control - Task management API - Direct desktop access - MCP protocol support ## 🚀 Your First Tasks Now let's see Bytebot in action! Try these example tasks: ### Simple Tasks (Test the Basics) "Take a screenshot of the desktop" "Open Firefox and go to google.com" "Create a text file called 'hello.txt' with today's date" "Check the system information and tell me the OS version" ### Advanced Tasks (See the Power) "Find the top 5 AI news stories today and create a summary document" "Go to hacker news, find the top 10 stories, and save them to a CSV file" "Upload a PDF contract and extract all payment terms and deadlines" "Search for 'machine learning tutorials', open the first 3 results in tabs, and take screenshots of each" ## Accessing Your Services | Service | URL | Purpose | | ---------------- | ------------------------------------------------------------------------ | --------------------------------------------- | | **Tasks UI** | [http://localhost:9992](http://localhost:9992) | Main interface for interacting with the agent | | **Agent API** | [http://localhost:9991/tasks](http://localhost:9991/tasks) | REST API for programmatic task creation | | **Desktop API** | [http://localhost:9990/computer-use](http://localhost:9990/computer-use) | Low-level desktop control API | | **MCP SSE** | [http://localhost:9990/mcp](http://localhost:9990/mcp) | Connect MCP clients for tool access | ## ☸️ Deploy with Helm See our [Helm deployment guide](/deployment/helm) for Kubernetes installation. ## 🖥️ Desktop Container Only If you just want the virtual desktop without the AI agent: ```bash # Using pre-built image (recommended) docker-compose -f docker/docker-compose.core.yml pull docker-compose -f docker/docker-compose.core.yml up -d ``` Or build locally: ```bash docker-compose -f docker/docker-compose.core.yml up -d --build ``` Access the desktop at [http://localhost:9990/vnc](http://localhost:9990/vnc) ## Managing Your Agent ### View Logs Monitor what your agent is doing: ```bash # All services docker-compose -f docker/docker-compose.yml logs -f # Just the agent docker-compose -f docker/docker-compose.yml logs -f bytebot-agent ``` ### Stop Services ```bash docker-compose -f docker/docker-compose.yml down ``` ### Update to Latest ```bash docker-compose -f docker/docker-compose.yml pull docker-compose -f docker/docker-compose.yml up -d ``` ### Reset Everything Remove all data and start fresh: ```bash docker-compose -f docker/docker-compose.yml down -v ``` ## Quick API Examples ### Create a Task via API ```bash # Simple task curl -X POST http://localhost:9991/tasks \ -H "Content-Type: application/json" \ -d '{ "description": "Search for flights from NYC to London next month", "priority": "MEDIUM" }' # Task with file upload curl -X POST http://localhost:9991/tasks \ -F "description=Read this contract and summarize the key terms" \ -F "priority=HIGH" \ -F "files=@contract.pdf" ``` ### Direct Desktop Control ```bash # Take a screenshot curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "screenshot"}' # Type text curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "type_text", "text": "Hello, Bytebot!"}' ``` ## Troubleshooting Check Docker is running and you have enough resources: ```bash docker info docker-compose -f docker/docker-compose.yml logs ``` Ensure all services are running: ```bash docker-compose -f docker/docker-compose.yml ps ``` All services should show as "Up". Check your API key is set correctly: ```bash cat docker/.env docker-compose -f docker/docker-compose.yml logs bytebot-agent ``` Ensure you're using a valid API key from Anthropic, OpenAI, or Google. ## 📚 Next Steps Learn how to create and manage tasks effectively Take control when you need to guide Bytebot Use any LLM provider with Bytebot Automate Bytebot with your applications ## 🔧 Configuration Options ### Environment Variables ```bash # Choose one AI provider: ANTHROPIC_API_KEY=sk-ant-... # For Claude models OPENAI_API_KEY=sk-... # For GPT models GEMINI_API_KEY=... # For Gemini models # Optional: Use specific models ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 # Default OPENAI_MODEL=gpt-4o GEMINI_MODEL=gemini-1.5-flash ``` ```bash # Change default ports if needed # Edit docker-compose.yml ports section: # bytebot-ui: # ports: # - "8080:9992" # Change 8080 to your desired port ``` ```bash # To use multiple LLM providers, use the proxy setup: docker-compose -f docker/docker-compose.proxy.yml up -d # This includes a pre-configured LiteLLM proxy ``` **Need help?** Join our [Discord community](https://discord.com/invite/d9ewZkWPTP) for support and to share what you're building! ================================================ FILE: docs/rest-api/computer-use.mdx ================================================ --- title: "Computer Action" openapi: "POST /computer-use" description: "Execute computer actions in the virtual desktop environment" --- Execute actions like mouse movements, clicks, keyboard input, and screenshots in the Bytebot desktop environment. ## Request The type of computer action to perform. Must be one of: `move_mouse`, `trace_mouse`, `click_mouse`, `press_mouse`, `drag_mouse`, `scroll`, `type_keys`, `press_keys`, `type_text`, `wait`, `screenshot`, `cursor_position`. ### Mouse Actions The target coordinates to move to. X coordinate (horizontal position) Y coordinate (vertical position) **Example Request** ```json { "action": "move_mouse", "coordinates": { "x": 100, "y": 200 } } ``` Array of coordinate objects for the mouse path. X coordinate Y coordinate {" "} Keys to hold while moving the mouse along the path. **Example Request** ```json { "action": "trace_mouse", "path": [ { "x": 100, "y": 100 }, { "x": 150, "y": 150 }, { "x": 200, "y": 200 } ], "holdKeys": ["shift"] } ``` The coordinates to click (uses current cursor position if omitted). X coordinate (horizontal position) Y coordinate (vertical position) {" "} Mouse button to click. Must be one of: `left`, `right`, `middle`. {" "} Number of clicks to perform. {" "} Keys to hold while clicking (e.g., ['ctrl', 'shift']) Key name **Example Request** ```json { "action": "click_mouse", "coordinates": { "x": 150, "y": 250 }, "button": "left", "clickCount": 2 } ``` The coordinates to press/release (uses current cursor position if omitted). X coordinate (horizontal position) Y coordinate (vertical position) {" "} Mouse button to press/release. Must be one of: `left`, `right`, `middle`. {" "} Whether to press or release the button. Must be one of: `up`, `down`. **Example Request** ```json { "action": "press_mouse", "coordinates": { "x": 150, "y": 250 }, "button": "left", "press": "down" } ``` Array of coordinate objects for the drag path. X coordinate Y coordinate {" "} Mouse button to use for dragging. Must be one of: `left`, `right`, `middle`. {" "} Keys to hold while dragging. **Example Request** ```json { "action": "drag_mouse", "path": [ { "x": 100, "y": 100 }, { "x": 200, "y": 200 } ], "button": "left" } ``` The coordinates to scroll at (uses current cursor position if omitted). X coordinate Y coordinate {" "} Scroll direction. Must be one of: `up`, `down`, `left`, `right`. {" "} Number of scroll steps to perform. {" "} Keys to hold while scrolling. **Example Request** ```json { "action": "scroll", "direction": "down", "scrollCount": 5 } ``` ### Keyboard Actions Array of keys to type in sequence. Key name {" "} Delay between key presses in milliseconds. **Example Request** ```json { "action": "type_keys", "keys": ["a", "b", "c", "enter"], "delay": 50 } ``` Array of keys to press or release. Key name {" "} Whether to press or release the keys. Must be one of: `up`, `down`. **Example Request** ```json { "action": "press_keys", "keys": ["ctrl", "shift", "esc"], "press": "down" } ``` The text string to type. {" "} Delay between characters in milliseconds. **Example Request** ```json { "action": "type_text", "text": "Hello, Bytebot!", "delay": 50 } ``` The text to paste. Useful for special characters that aren't on the standard keyboard. **Example Request** ```json { "action": "paste_text", "text": "Special characters: ©®™€¥£ émojis 🎉" } ``` ### System Actions Wait duration in milliseconds. **Example Request** ```json { "action": "wait", "duration": 2000 } ``` No parameters required. **Example Request** ```json { "action": "screenshot" } ``` No parameters required. **Example Request** ```json { "action": "cursor_position" } ``` The application to switch to. Available options: `firefox`, `1password`, `thunderbird`, `vscode`, `terminal`, `desktop`, `directory`. **Example Request** ```json { "action": "application", "application": "firefox" } ``` **Available Applications:** - `firefox` - Mozilla Firefox web browser - `1password` - Password manager - `thunderbird` - Email client - `vscode` - Visual Studio Code editor - `terminal` - Terminal/console application - `desktop` - Switch to desktop - `directory` - File manager/directory browser ## Response Responses vary based on the action performed: ### Default Response Most actions return a simple success response: ```json { "success": true } ``` ### Screenshot Response Returns the screenshot as a base64 encoded string: ```json { "success": true, "data": { "image": "base64_encoded_image_data" } } ``` ### Cursor Position Response Returns the current cursor position: ```json { "success": true, "data": { "x": 123, "y": 456 } } ``` ### Error Response ```json { "success": false, "error": "Error message" } ``` ### Code Examples ```bash cURL curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "move_mouse", "coordinates": {"x": 100, "y": 200}}' ``` ```python Python import requests def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() # Move the mouse result = control_computer("move_mouse", coordinates={"x": 100, "y": 100}) print(result) ``` ```javascript JavaScript const axios = require("axios"); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; const response = await axios.post(url, data); return response.data; } // Move mouse example controlComputer("move_mouse", { coordinates: { x: 100, y: 100 } }) .then((result) => console.log(result)) .catch((error) => console.error("Error:", error)); ``` ================================================ FILE: docs/rest-api/examples.mdx ================================================ --- title: "Usage Examples" description: "Code examples for common automation scenarios using the Bytebot REST API" --- This page provides practical examples of how to use the Bytebot REST API in different programming languages and scenarios. ## Language Examples ### cURL Examples ```bash Open Application and Navigate # Open an application (like Firefox) curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "move_mouse", "coordinates": {"x": 100, "y": 950}}' curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "click_mouse", "button": "left", "clickCount": 2}' # Wait for application to open curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "wait", "duration": 150}' # Type URL in address bar curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "type_text", "text": "https://example.com"}' # Press Enter to navigate curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "typ_keys", "keys": ["enter"]}' ```` ```bash Take and Save Screenshot # Take a screenshot response=$(curl -s -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "screenshot"}') # Extract the base64 image data and save to a file echo $response | jq -r '.data.image' | base64 -d > screenshot.png echo "Screenshot saved to screenshot.png" ```` ```bash Copy and Paste Text # Select text with triple click (selects a paragraph) curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "move_mouse", "coordinates": {"x": 400, "y": 300}}' curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "click_mouse", "button": "left", "clickCount": 3}' # Copy with Ctrl+C curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "press_keys", "keys": ["ctrl", "c"], "press": "down"}' curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "press_keys", "keys": ["ctrl", "c"], "press": "up"}' # Click elsewhere to paste curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "move_mouse", "coordinates": {"x": 400, "y": 500}}' curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "click_mouse", "button": "left"}' # Paste with Ctrl+V curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "press_keys", "keys": ["ctrl", "v"], "press": "down"}' curl -X POST http://localhost:9990/computer-use \ -H "Content-Type: application/json" \ -d '{"action": "press_keys", "keys": ["ctrl", "v"], "press": "up"}' ``` ### Python Examples ```python Web Form Automation import requests import time def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def fill_web_form(): # Navigate to a form (e.g., login form) control_computer("move_mouse", coordinates={"x": 500, "y": 300}) control_computer("click_mouse", button="left") # Type username control_computer("type_text", text="user@example.com") # Tab to password field control_computer("type_keys", keys=["tab"]) # Type password control_computer("type_text", text="secure_password") # Tab to login button control_computer("type_keys", keys=["tab"]) # Press Enter to submit control_computer("type_keys", keys=["enter"]) # Wait for page to load control_computer("wait", duration=2000) print("Form submitted successfully") # Run the automation # fill_web_form() ```` ```python File Upload Dialog import requests import time def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def upload_file(file_path="/path/to/file.txt"): # Click the upload button control_computer("move_mouse", coordinates={"x": 400, "y": 300}) control_computer("click_mouse", button="left") # Wait for file dialog to appear control_computer("wait", duration=1000) # Type the file path control_computer("type_text", text=file_path) # Press Enter to confirm control_computer("press_keys", keys=["enter"], press="down") control_computer("press_keys", keys=["enter"], press="up") # Wait for upload to complete control_computer("wait", duration=3000) # Take screenshot of result result = control_computer("screenshot") if result["success"]: print("File upload completed and screenshot taken") # Run the automation # upload_file("/Users/username/Documents/example.pdf") ```` ```python Screenshot Monitoring import requests import base64 import time from io import BytesIO from PIL import Image def take_screenshot(): url = "http://localhost:9990/computer-use" response = requests.post(url, json={"action": "screenshot"}) if response.json()["success"]: img_data = base64.b64decode(response.json()["data"]["image"]) return Image.open(BytesIO(img_data)) return None def monitor_for_changes(interval=5, duration=60): """Monitor the screen for changes at regular intervals""" first_screenshot = take_screenshot() if not first_screenshot: print("Failed to take initial screenshot") return first_screenshot.save("baseline.png") print("Baseline screenshot saved") end_time = time.time() + duration screenshot_count = 1 while time.time() < end_time: time.sleep(interval) current = take_screenshot() if current: filename = f"screenshot_{screenshot_count}.png" current.save(filename) print(f"Saved {filename}") screenshot_count += 1 print(f"Monitoring completed. Saved {screenshot_count} screenshots.") # Run the monitoring for 30 seconds, taking a screenshot every 5 seconds # monitor_for_changes(interval=5, duration=30) ``` ### JavaScript/Node.js Examples ```javascript Browser Navigation const axios = require('axios'); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; try { const response = await axios.post(url, data); return response.data; } catch (error) { console.error('Error:', error.message); throw error; } } async function navigateToWebsite(url) { console.log(`Navigating to ${url}...`); // Open Firefox/Chrome by clicking on dock icon await controlComputer("move_mouse", { coordinates: { x: 100, y: 950 } }); await controlComputer("click_mouse", { button: "left" }); // Wait for browser to open await controlComputer("wait", { duration: 2000 }); // Click in URL bar (usually near the top) await controlComputer("move_mouse", { coordinates: { x: 400, y: 60 } }); await controlComputer("click_mouse", { button: "left" }); // Select all existing text (Cmd+A on Mac, Ctrl+A elsewhere) await controlComputer("press_keys", { keys: ["ctrl"], press: "down" }); await controlComputer("press_keys", { keys: ["a"], press: "down" }); await controlComputer("press_keys", { keys: ["a"], press: "up" }); await controlComputer("press_keys", { keys: ["ctrl"], press: "up" }); // Type the URL await controlComputer("type_text", { text: url }); // Press Enter to navigate await controlComputer("press_keys", { keys: ["enter"], press: "down" }); await controlComputer("press_keys", { keys: ["enter"], press: "up" }); // Wait for page to load await controlComputer("wait", { duration: 3000 }); console.log("Navigation completed"); } // Usage // navigateToWebsite("https://example.com").catch(console.error); ```` ```javascript Data Entry Automation const axios = require('axios'); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; try { const response = await axios.post(url, data); return response.data; } catch (error) { console.error('Error:', error.message); throw error; } } // Function to fill a data entry form async function fillDataEntryForm(formData) { // Click on the first form field await controlComputer("move_mouse", { coordinates: { x: 400, y: 250 } }); await controlComputer("click_mouse", { button: "left" }); // Fill each field and press Tab to move to the next for (const [index, value] of formData.entries()) { // Type the value await controlComputer("type_text", { text: value }); // If not the last field, press Tab to move to next field if (index < formData.length - 1) { await controlComputer("press_keys", { keys: ["tab"], press: "down" }); await controlComputer("press_keys", { keys: ["tab"], press: "up" }); await controlComputer("wait", { duration: 300 }); } } // Find and click the submit button await controlComputer("move_mouse", { coordinates: { x: 400, y: 500 } }); await controlComputer("click_mouse", { button: "left" }); // Take a screenshot of the result const result = await controlComputer("screenshot"); console.log("Form submitted successfully"); return result; } // Example form data const formFields = [ "John Doe", // Name "john.doe@example.com", // Email "123 Main St", // Address "555-123-4567" // Phone ]; // Usage // fillDataEntryForm(formFields).catch(console.error); ```` ```javascript UI Testing const axios = require("axios"); const fs = require("fs"); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; try { const response = await axios.post(url, data); return response.data; } catch (error) { console.error("Error:", error.message); throw error; } } // Simple UI testing framework async function testUIElement(name, options) { const { x, y, expectedResult } = options; console.log(`Testing UI element: ${name}`); // Take screenshot before interaction const beforeShot = await controlComputer("screenshot"); fs.writeFileSync( `before_${name}.png`, Buffer.from(beforeShot.data.image, "base64") ); // Click the element await controlComputer("move_mouse", { coordinates: { x, y } }); await controlComputer("click_mouse", { button: "left" }); // Wait for any UI changes await controlComputer("wait", { duration: 1000 }); // Take screenshot after interaction const afterShot = await controlComputer("screenshot"); fs.writeFileSync( `after_${name}.png`, Buffer.from(afterShot.data.image, "base64") ); console.log(`Test for ${name} completed. Screenshots saved.`); if (expectedResult && typeof expectedResult === "function") { // Call custom verification function if provided await expectedResult(); } } // Example UI elements to test const uiElements = [ { name: "login_button", x: 500, y: 400 }, { name: "menu_icon", x: 50, y: 50 }, { name: "close_dialog", x: 800, y: 200 }, ]; // Run tests sequentially async function runUITests() { for (const element of uiElements) { await testUIElement(element.name, element); // Add some delay between tests await controlComputer("wait", { duration: 2000 }); } console.log("All UI tests completed"); } // Usage // runUITests().catch(console.error); ``` ### Common Automation Scenarios ### Browser Automation Workflow This example demonstrates a complete browser workflow, opening a site and interacting with it: ```python import requests import time def control_computer(action, **params): url = "http://localhost:9990/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def browser_workflow(): # Open browser (assuming browser icon is at position x=100, y=960) control_computer("move_mouse", coordinates={"x": 100, "y": 960}) control_computer("click_mouse", button="left") time.sleep(3) # Wait for browser to open # Type URL and navigate control_computer("type_text", text="https://example.com") control_computer("press_keys", key="enter") time.sleep(2) # Wait for page to load # Take screenshot of the loaded page screenshot = control_computer("screenshot") # Click on a link (coordinates would need to be adjusted for your target) control_computer("move_mouse", coordinates={"x": 300, "y": 400}) control_computer("click_mouse", button="left") time.sleep(2) # Scroll down control_computer("scroll", direction="down", amount=500) # Fill a search box control_computer("move_mouse", coordinates={"x": 600, "y": 200}) control_computer("click_mouse", button="left") control_computer("type_text", text="search query") control_computer("press_keys", key="enter") browser_workflow() ``` ### Form Filling Workflow This example shows a complete form-filling process: ```javascript const axios = require("axios"); async function controlComputer(action, params = {}) { const url = "http://localhost:9990/computer-use"; const data = { action, ...params }; const response = await axios.post(url, data); return response.data; } async function fillForm() { // Navigate to form page await controlComputer("move_mouse", { coordinates: { x: 100, y: 960 } }); await controlComputer("click_mouse", { button: "left" }); await controlComputer("wait", { duration: 3000 }); await controlComputer("type_text", { text: "https://example.com/form" }); await controlComputer("press_keys", { key: "enter" }); await controlComputer("wait", { duration: 2000 }); // Fill form // Name field await controlComputer("move_mouse", { coordinates: { x: 400, y: 250 } }); await controlComputer("click_mouse", { button: "left" }); // Type the value await controlComputer("type_text", { text: "John Doe" }); // Email field (tab to next field) await controlComputer("press_keys", { keys: ["tab"], press: "down" }); await controlComputer("press_keys", { keys: ["tab"], press: "up" }); await controlComputer("type_text", { text: "john@example.com" }); // Message field (tab to next field) await controlComputer("press_keys", { keys: ["tab"], press: "down" }); await controlComputer("press_keys", { keys: ["tab"], press: "up" }); await controlComputer("type_text", { text: "This is an automated message sent using Bytebot's Computer Use API", delay: 30, }); // Submit form await controlComputer("press_keys", { keys: ["tab"], press: "down" }); await controlComputer("press_keys", { keys: ["tab"], press: "up" }); await controlComputer("press_keys", { key: "enter" }); // Take screenshot of confirmation page await controlComputer("wait", { duration: 2000 }); const screenshot = await controlComputer("screenshot"); console.log("Form submitted successfully"); } fillForm().catch(console.error); ``` ### Automation Framework Integration You can create a reusable automation framework with Bytebot: ```python import requests import time import json class BytebotDriver: """A Selenium-like driver for Bytebot""" def __init__(self, base_url="http://localhost:9990"): self.base_url = base_url def control_computer(self, action, **params): url = f"{self.base_url}/computer-use" data = {"action": action, **params} response = requests.post(url, json=data) return response.json() def open_browser(self, browser_icon_coords): """Open a browser by clicking its icon""" self.control_computer("move_mouse", coordinates=browser_icon_coords) self.control_computer("click_mouse", button="left") time.sleep(3) # Wait for browser to open def navigate_to(self, url): """Navigate to a URL in the browser""" self.control_computer("type_text", text=url) self.control_computer("press_keys", key="enter") time.sleep(2) # Wait for page to load def click_element(self, coords): """Click an element at the specified coordinates""" self.control_computer("move_mouse", coordinates=coords) self.control_computer("click_mouse", button="left") def type_text(self, text): """Type text at the current cursor position""" self.control_computer("type_text", text=text) def press_key(self, key, modifiers=None): """Press a keyboard key with optional modifiers""" params = {"key": key} if modifiers: params["modifiers"] = modifiers self.control_computer("press_keys", **params) def take_screenshot(self): """Take a screenshot of the desktop""" return self.control_computer("screenshot") def scroll(self, direction, amount): """Scroll in the specified direction""" self.control_computer("scroll", direction=direction, amount=amount) # Example usage driver = BytebotDriver() driver.open_browser({"x": 100, "y": 960}) driver.navigate_to("https://example.com") driver.click_element({"x": 300, "y": 400}) driver.type_text("Hello Bytebot!") driver.press_key("enter") result = driver.take_screenshot() print(f"Screenshot captured: {result['success']}") ``` ================================================ FILE: docs/rest-api/input-tracking.mdx ================================================ --- title: "Input Tracking" openapi: "POST /input-tracking/start" description: "Start and stop input tracking on the Bytebot desktop" --- The Bytebot daemon can monitor mouse and keyboard events through the `InputTracking` module. Tracking is disabled by default and can be toggled via the REST API. Tracked actions are streamed over WebSockets so that the agent can store them as messages. ## Start Tracking `POST /input-tracking/start` Begins capturing input events. The endpoint returns a simple status object: ```json { "status": "started" } ``` ## Stop Tracking `POST /input-tracking/stop` Stops capturing events and clears any internal buffers. The response is similar to the start endpoint: ```json { "status": "stopped" } ``` ## WebSocket Stream When tracking is active, actions are emitted on the `input_action` channel of the WebSocket server running on the daemon. Clients can connect to the daemon and listen for these events to persist them as needed. ================================================ FILE: docs/rest-api/introduction.mdx ================================================ --- title: "Introduction" description: "Overview of the Bytebot REST API" --- ## Bytebot REST API Bytebot's core functionality is exposed through its REST API, which provides endpoints for interacting with the desktop environment. The API allows for programmatic control of mouse movement, keyboard input, and screen capture. ### Base URL All API endpoints are relative to the base URL: ``` http://localhost:9990 ``` The port can be configured when running the container. ### Authentication The Bytebot API does not require authentication by default when accessed locally. For remote access, standard network security practices should be implemented. ### Response Format All API responses follow a standard JSON format: ```json { "success": true, "data": { ... }, // Response data specific to the action "error": null // Error message if success is false } ``` ### Error Handling When an error occurs, the API returns: ```json { "success": false, "data": null, "error": "Detailed error message" } ``` Common HTTP status codes: | Status Code | Description | | ----------- | -------------------------------- | | 200 | Success | | 400 | Bad Request - Invalid parameters | | 500 | Internal Server Error | ### Available Endpoints Execute desktop automation actions like mouse movements, clicks, keyboard input, and screenshots Control and stream keyboard and mouse events from the desktop Code examples and snippets for common automation scenarios ### Rate Limiting The API currently does not implement rate limiting, but excessive requests may impact performance of the virtual desktop environment. ================================================ FILE: helm/Chart.yaml ================================================ apiVersion: v2 name: bytebot description: Bytebot - Complete deployment package type: application version: 0.1.0 appVersion: "edge" dependencies: - name: postgresql version: 0.1.0 repository: file://./charts/postgresql condition: postgresql.enabled - name: bytebot-desktop version: 0.1.0 repository: file://./charts/bytebot-desktop - name: bytebot-agent version: 0.1.0 repository: file://./charts/bytebot-agent - name: bytebot-ui version: 0.1.0 repository: file://./charts/bytebot-ui - name: bytebot-llm-proxy version: 0.1.0 repository: file://./charts/bytebot-llm-proxy condition: bytebot-llm-proxy.enabled keywords: - bytebot - automation - remote-desktop maintainers: - name: Bytebot Team sources: - https://github.com/bytebot-ai/bytebot ================================================ FILE: helm/README.md ================================================ # Bytebot Helm Charts This directory contains Helm charts for deploying Bytebot on Kubernetes. ## Documentation For complete deployment instructions, see: **[Helm Deployment Guide](https://docs.bytebot.ai/deployment/helm)** ## Quick Start ```bash # Clone repository git clone https://github.com/bytebot-ai/bytebot.git cd bytebot # Create values.yaml with your API key(s) cat > values.yaml <('BYTEBOT_ANALYTICS_ENDPOINT'); if (!this.endpoint) { this.logger.warn( 'BYTEBOT_ANALYTICS_ENDPOINT is not set. Analytics service disabled.', ); } } @OnEvent('task.cancel') @OnEvent('task.failed') @OnEvent('task.completed') async handleTaskEvent(payload: { taskId: string }) { if (!this.endpoint) return; try { const task = await this.tasksService.findById(payload.taskId); const messages = await this.messagesService.findEvery(payload.taskId); await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...task, messages }), }); } catch (error: any) { this.logger.error( `Failed to send analytics for task ${payload.taskId}: ${error.message}`, error.stack, ); } } } ================================================ FILE: packages/bytebot-agent/src/agent/agent.computer-use.ts ================================================ import { Button, Coordinates, Press, ComputerToolUseContentBlock, ToolResultContentBlock, MessageContentType, isScreenshotToolUseBlock, isCursorPositionToolUseBlock, isMoveMouseToolUseBlock, isTraceMouseToolUseBlock, isClickMouseToolUseBlock, isPressMouseToolUseBlock, isDragMouseToolUseBlock, isScrollToolUseBlock, isTypeKeysToolUseBlock, isPressKeysToolUseBlock, isTypeTextToolUseBlock, isWaitToolUseBlock, isApplicationToolUseBlock, isPasteTextToolUseBlock, isReadFileToolUseBlock, } from '@bytebot/shared'; import { Logger } from '@nestjs/common'; const BYTEBOT_DESKTOP_BASE_URL = process.env.BYTEBOT_DESKTOP_BASE_URL as string; export async function handleComputerToolUse( block: ComputerToolUseContentBlock, logger: Logger, ): Promise { logger.debug( `Handling computer tool use: ${block.name}, tool_use_id: ${block.id}`, ); if (isScreenshotToolUseBlock(block)) { logger.debug('Processing screenshot request'); try { logger.debug('Taking screenshot'); const image = await screenshot(); logger.debug('Screenshot captured successfully'); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Image, source: { data: image, media_type: 'image/png', type: 'base64', }, }, ], }; } catch (error) { logger.error(`Screenshot failed: ${error.message}`, error.stack); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'ERROR: Failed to take screenshot', }, ], is_error: true, }; } } if (isCursorPositionToolUseBlock(block)) { logger.debug('Processing cursor position request'); try { logger.debug('Getting cursor position'); const position = await cursorPosition(); logger.debug(`Cursor position obtained: ${position.x}, ${position.y}`); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: `Cursor position: ${position.x}, ${position.y}`, }, ], }; } catch (error) { logger.error( `Getting cursor position failed: ${error.message}`, error.stack, ); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'ERROR: Failed to get cursor position', }, ], is_error: true, }; } } try { if (isMoveMouseToolUseBlock(block)) { await moveMouse(block.input); } if (isTraceMouseToolUseBlock(block)) { await traceMouse(block.input); } if (isClickMouseToolUseBlock(block)) { await clickMouse(block.input); } if (isPressMouseToolUseBlock(block)) { await pressMouse(block.input); } if (isDragMouseToolUseBlock(block)) { await dragMouse(block.input); } if (isScrollToolUseBlock(block)) { await scroll(block.input); } if (isTypeKeysToolUseBlock(block)) { await typeKeys(block.input); } if (isPressKeysToolUseBlock(block)) { await pressKeys(block.input); } if (isTypeTextToolUseBlock(block)) { await typeText(block.input); } if (isPasteTextToolUseBlock(block)) { await pasteText(block.input); } if (isWaitToolUseBlock(block)) { await wait(block.input); } if (isApplicationToolUseBlock(block)) { await application(block.input); } if (isReadFileToolUseBlock(block)) { logger.debug(`Reading file: ${block.input.path}`); const result = await readFile(block.input); if (result.success && result.data) { // Return document content block return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Document, source: { type: 'base64', media_type: result.mediaType || 'application/octet-stream', data: result.data, }, name: result.name || 'file', size: result.size, }, ], }; } else { // Return error message return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: result.message || 'Error reading file', }, ], is_error: true, }; } } let image: string | null = null; try { // Wait before taking screenshot to allow UI to settle const delayMs = 750; // 750ms delay logger.debug(`Waiting ${delayMs}ms before taking screenshot`); await new Promise((resolve) => setTimeout(resolve, delayMs)); logger.debug('Taking screenshot'); image = await screenshot(); logger.debug('Screenshot captured successfully'); } catch (error) { logger.error('Failed to take screenshot', error); } logger.debug(`Tool execution successful for tool_use_id: ${block.id}`); const toolResult: ToolResultContentBlock = { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'Tool executed successfully', }, ], }; if (image) { toolResult.content.push({ type: MessageContentType.Image, source: { data: image, media_type: 'image/png', type: 'base64', }, }); } return toolResult; } catch (error) { logger.error( `Error executing ${block.name} tool: ${error.message}`, error.stack, ); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: `Error executing ${block.name} tool: ${error.message}`, }, ], is_error: true, }; } } async function moveMouse(input: { coordinates: Coordinates }): Promise { const { coordinates } = input; console.log( `Moving mouse to coordinates: [${coordinates.x}, ${coordinates.y}]`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'move_mouse', coordinates, }), }); } catch (error) { console.error('Error in move_mouse action:', error); throw error; } } async function traceMouse(input: { path: Coordinates[]; holdKeys?: string[]; }): Promise { const { path, holdKeys } = input; console.log( `Tracing mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'trace_mouse', path, holdKeys, }), }); } catch (error) { console.error('Error in trace_mouse action:', error); throw error; } } async function clickMouse(input: { coordinates?: Coordinates; button: Button; holdKeys?: string[]; clickCount: number; }): Promise { const { coordinates, button, holdKeys, clickCount } = input; console.log( `Clicking mouse ${button} ${clickCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}] ` : ''} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'click_mouse', coordinates, button, holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined, clickCount, }), }); } catch (error) { console.error('Error in click_mouse action:', error); throw error; } } async function pressMouse(input: { coordinates?: Coordinates; button: Button; press: Press; }): Promise { const { coordinates, button, press } = input; console.log( `Pressing mouse ${button} ${press} ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'press_mouse', coordinates, button, press, }), }); } catch (error) { console.error('Error in press_mouse action:', error); throw error; } } async function dragMouse(input: { path: Coordinates[]; button: Button; holdKeys?: string[]; }): Promise { const { path, button, holdKeys } = input; console.log( `Dragging mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'drag_mouse', path, button, holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined, }), }); } catch (error) { console.error('Error in drag_mouse action:', error); throw error; } } async function scroll(input: { coordinates?: Coordinates; direction: 'up' | 'down' | 'left' | 'right'; scrollCount: number; holdKeys?: string[]; }): Promise { const { coordinates, direction, scrollCount, holdKeys } = input; console.log( `Scrolling ${direction} ${scrollCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'scroll', coordinates, direction, scrollCount, holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined, }), }); } catch (error) { console.error('Error in scroll action:', error); throw error; } } async function typeKeys(input: { keys: string[]; delay?: number; }): Promise { const { keys, delay } = input; console.log(`Typing keys: ${keys}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'type_keys', keys, delay, }), }); } catch (error) { console.error('Error in type_keys action:', error); throw error; } } async function pressKeys(input: { keys: string[]; press: Press; }): Promise { const { keys, press } = input; console.log(`Pressing keys: ${keys}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'press_keys', keys, press, }), }); } catch (error) { console.error('Error in press_keys action:', error); throw error; } } async function typeText(input: { text: string; delay?: number; }): Promise { const { text, delay } = input; console.log(`Typing text: ${text}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'type_text', text, delay, }), }); } catch (error) { console.error('Error in type_text action:', error); throw error; } } async function pasteText(input: { text: string }): Promise { const { text } = input; console.log(`Pasting text: ${text}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'paste_text', text, }), }); } catch (error) { console.error('Error in paste_text action:', error); throw error; } } async function wait(input: { duration: number }): Promise { const { duration } = input; console.log(`Waiting for ${duration}ms`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'wait', duration, }), }); } catch (error) { console.error('Error in wait action:', error); throw error; } } async function cursorPosition(): Promise { console.log('Getting cursor position'); try { const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'cursor_position', }), }); const data = await response.json(); return { x: data.x, y: data.y }; } catch (error) { console.error('Error in cursor_position action:', error); throw error; } } async function screenshot(): Promise { console.log('Taking screenshot'); try { const requestBody = { action: 'screenshot', }; const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`Failed to take screenshot: ${response.statusText}`); } const data = await response.json(); if (!data.image) { throw new Error('Failed to take screenshot: No image data received'); } return data.image; // Base64 encoded image } catch (error) { console.error('Error in screenshot action:', error); throw error; } } async function application(input: { application: string }): Promise { const { application } = input; console.log(`Opening application: ${application}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'application', application, }), }); } catch (error) { console.error('Error in application action:', error); throw error; } } async function readFile(input: { path: string }): Promise<{ success: boolean; data?: string; name?: string; size?: number; mediaType?: string; message?: string; }> { const { path } = input; console.log(`Reading file: ${path}`); try { const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'read_file', path, }), }); if (!response.ok) { throw new Error(`Failed to read file: ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { console.error('Error in read_file action:', error); return { success: false, message: `Error reading file: ${error.message}`, }; } } export async function writeFile(input: { path: string; content: string; }): Promise<{ success: boolean; message?: string }> { const { path, content } = input; console.log(`Writing file: ${path}`); try { // Content is always base64 encoded const base64Data = content; const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'write_file', path, data: base64Data, }), }); if (!response.ok) { throw new Error(`Failed to write file: ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { console.error('Error in write_file action:', error); return { success: false, message: `Error writing file: ${error.message}`, }; } } ================================================ FILE: packages/bytebot-agent/src/agent/agent.constants.ts ================================================ export const DEFAULT_DISPLAY_SIZE = { width: 1280, height: 960, }; export const SUMMARIZATION_SYSTEM_PROMPT = `You are a helpful assistant that summarizes conversations for long-running tasks. Your job is to create concise summaries that preserve all important information, tool usage, and key decisions. Focus on: - Task progress and completed actions - Important tool calls and their results - Key decisions made - Any errors or issues encountered - Current state and what remains to be done Provide a structured summary that can be used as context for continuing the task.`; export const AGENT_SYSTEM_PROMPT = ` You are **Bytebot**, a highly-reliable AI engineer operating a virtual computer whose display measures ${DEFAULT_DISPLAY_SIZE.width} x ${DEFAULT_DISPLAY_SIZE.height} pixels. The current date is ${new Date().toLocaleDateString()}. The current time is ${new Date().toLocaleTimeString()}. The current timezone is ${Intl.DateTimeFormat().resolvedOptions().timeZone}. ──────────────────────── AVAILABLE APPLICATIONS ──────────────────────── On the computer, the following applications are available: Firefox Browser -- The default web browser, use it to navigate to websites. Thunderbird -- The default email client, use it to send and receive emails (if you have an account). 1Password -- The password manager, use it to store and retrieve your passwords (if you have an account). Visual Studio Code -- The default code editor, use it to create and edit files. Terminal -- The default terminal, use it to run commands. File Manager -- The default file manager, use it to navigate and manage files. Trash -- The default trash ALL APPLICATIONS ARE GUI BASED, USE THE COMPUTER TOOLS TO INTERACT WITH THEM. ONLY ACCESS THE APPLICATIONS VIA THEIR DESKTOP ICONS. *Never* use keyboard shortcuts to switch between applications, only use \`computer_application\` to switch between the default applications. ──────────────────────── CORE WORKING PRINCIPLES ──────────────────────── 1. **Observe First** - *Always* invoke \`computer_screenshot\` before your first action **and** whenever the UI may have changed. Screenshot before every action when filling out forms. Never act blindly. When opening documents or PDFs, scroll through at least the first page to confirm it is the correct document. 2. **Navigate applications** = *Always* invoke \`computer_application\` to switch between the default applications. 3. **Human-Like Interaction** • Move in smooth, purposeful paths; click near the visual centre of targets. • Double-click desktop icons to open them. • Type realistic, context-appropriate text with \`computer_type_text\` (for short strings) or \`computer_paste_text\` (for long strings), or shortcuts with \`computer_type_keys\`. 4. **Valid Keys Only** - Use **exactly** the identifiers listed in **VALID KEYS** below when supplying \`keys\` to \`computer_type_keys\` or \`computer_press_keys\`. All identifiers come from nut-tree's \`Key\` enum; they are case-sensitive and contain *no spaces*. 5. **Verify Every Step** - After each action: a. Take another screenshot. b. Confirm the expected state before continuing. If it failed, retry sensibly (try again, and then try 2 different methods) before calling \`set_task_status\` with \`"status":"needs_help"\`. 6. **Efficiency & Clarity** - Combine related key presses; prefer scrolling or dragging over many small moves; minimise unnecessary waits. 7. **Stay Within Scope** - Do nothing the user didn't request; don't suggest unrelated tasks. For form and login fields, don't fill in random data, unless explicitly told to do so. 8. **Security** - If you see a password, secret key, or other sensitive information (or the user shares it with you), do not repeat it in conversation. When typing sensitive information, use \`computer_type_text\` with \`isSensitive\` set to \`true\`. 9. **Consistency & Persistence** - Even if the task is repetitive, do not end the task until the user's goal is completely met. For bulk operations, maintain focus and continue until all items are processed. ──────────────────────── REPETITIVE TASK HANDLING ──────────────────────── When performing repetitive tasks (e.g., "visit each profile", "process all items"): 1. **Track Progress** - Maintain a mental count of: • Total items to process (if known) • Items completed so far • Current item being processed • Any errors encountered 2. **Batch Processing** - For large sets: • Process in groups of 10-20 items • Take brief pauses between batches to prevent system overload • Continue until ALL items are processed 3. **Error Recovery** - If an item fails: • Note the error but continue with the next item • Keep a list of failed items to report at the end • Don't let one failure stop the entire operation 4. **Progress Updates** - Every 10-20 items: • Brief status: "Processed 20/100 profiles, continuing..." • No need for detailed reports unless requested 5. **Completion Criteria** - The task is NOT complete until: • All items in the set are processed, OR • You reach a clear endpoint (e.g., "No more profiles to load"), OR • The user explicitly tells you to stop 6. **State Management** - If the task might span multiple tabs/pages: • Save progress to a file periodically • Include timestamps and item identifiers ──────────────────────── TASK LIFECYCLE TEMPLATE ──────────────────────── 1. **Prepare** - Initial screenshot → plan → estimate scope if possible. 2. **Execute Loop** - For each sub-goal: Screenshot → Think → Act → Verify. 3. **Batch Loop** - For repetitive tasks: • While items remain: - Process batch of 10-20 items - Update progress counter - Check for stop conditions - Brief status update • Continue until ALL done 4. **Switch Applications** - If you need to switch between the default applications, reach the home directory, or return to the desktop, invoke \`\`\`json { "name": "computer_application", "input": { "application": "application name" } } \`\`\` It will open (or focus if it is already open) the application, in fullscreen. The application name must be one of the following: firefox, thunderbird, 1password, vscode, terminal, directory, desktop. 5. **Create other tasks** - If you need to create additional separate tasks, invoke \`\`\`json { "name": "create_task", "input": { "description": "Subtask description", "type": "IMMEDIATE", "priority": "MEDIUM" } } \`\`\` The other tasks will be executed in the order they are created, after the current task is completed. Only create separate tasks if they are not related to the current task. 6. **Schedule future tasks** - If you need to schedule a task to run in the future, invoke \`\`\`json { "name": "create_task", "input": { "description": "Subtask description", "type": "SCHEDULED", "scheduledFor": , "priority": "MEDIUM" } } \`\`\` Only schedule tasks if they must be run in the future. Do not schedule tasks that can be run immediately. 7. **Read Files** - If you need to read file contents, invoke \`\`\`json { "name": "computer_read_file", "input": { "path": "/path/to/file" } } \`\`\` This tool reads files and returns them as document content blocks with base64 data, supporting various file types including documents (PDF, DOCX, TXT, etc.) and images (PNG, JPG, etc.). 8. **Ask for Help** - If you need clarification, or if you are unable to fully complete the task, invoke \`\`\`json { "name": "set_task_status", "input": { "status": "needs_help", "description": "Summary of help or clarification needed" } } \`\`\` 9. **Cleanup** - When the user's goal is met: • Close every window, file, or app you opened so the desktop is tidy. • Return to an idle desktop/background. 10. **Terminate** - ONLY ONCE THE USER'S GOAL IS COMPLETELY MET, As your final tool call and message, invoke \`\`\`json { "name": "set_task_status", "input": { "status": "completed", "description": "Summary of the task" } } \`\`\` No further actions or messages will follow this call. **IMPORTANT**: For bulk operations like "visit each profile in the directory": - Do NOT mark as completed after just a few profiles - Continue until you've processed ALL profiles or reached a clear end - If there are 100+ profiles, process them ALL - Only stop when explicitly told or when there are genuinely no more items ──────────────────────── VALID KEYS ──────────────────────── A, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp, B, Backslash, Backspace, C, CapsLock, Clear, Comma, D, Decimal, Delete, Divide, Down, E, End, Enter, Equal, Escape, F, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, Fn, G, Grave, H, Home, I, Insert, J, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin, M, Menu, Minus, Multiply, N, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock, NumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9, O, P, PageDown, PageUp, Pause, Period, Print, Q, Quote, R, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin, S, ScrollLock, Semicolon, Slash, Space, Subtract, T, Tab, U, Up, V, W, X, Y, Z Remember: **accuracy over speed, clarity and consistency over cleverness**. Think before each move, keep the desktop clean when you're done, and **always** finish with \`set_task_status\`. Don't ask follow-up questions after completing the task. **For repetitive tasks**: Persistence is key. Continue until ALL items are processed, not just the first few. `; ================================================ FILE: packages/bytebot-agent/src/agent/agent.module.ts ================================================ import { Module } from '@nestjs/common'; import { TasksModule } from '../tasks/tasks.module'; import { MessagesModule } from '../messages/messages.module'; import { AnthropicModule } from '../anthropic/anthropic.module'; import { AgentProcessor } from './agent.processor'; import { ConfigModule } from '@nestjs/config'; import { AgentScheduler } from './agent.scheduler'; import { InputCaptureService } from './input-capture.service'; import { OpenAIModule } from '../openai/openai.module'; import { GoogleModule } from '../google/google.module'; import { SummariesModule } from 'src/summaries/summaries.modue'; import { AgentAnalyticsService } from './agent.analytics'; import { ProxyModule } from 'src/proxy/proxy.module'; @Module({ imports: [ ConfigModule, TasksModule, MessagesModule, SummariesModule, AnthropicModule, OpenAIModule, GoogleModule, ProxyModule, ], providers: [ AgentProcessor, AgentScheduler, InputCaptureService, AgentAnalyticsService, ], exports: [AgentProcessor], }) export class AgentModule {} ================================================ FILE: packages/bytebot-agent/src/agent/agent.processor.ts ================================================ import { TasksService } from '../tasks/tasks.service'; import { MessagesService } from '../messages/messages.service'; import { Injectable, Logger } from '@nestjs/common'; import { Message, Role, Task, TaskPriority, TaskStatus, TaskType, } from '@prisma/client'; import { AnthropicService } from '../anthropic/anthropic.service'; import { isComputerToolUseContentBlock, isSetTaskStatusToolUseBlock, isCreateTaskToolUseBlock, SetTaskStatusToolUseBlock, } from '@bytebot/shared'; import { MessageContentBlock, MessageContentType, ToolResultContentBlock, TextContentBlock, } from '@bytebot/shared'; import { InputCaptureService } from './input-capture.service'; import { OnEvent } from '@nestjs/event-emitter'; import { OpenAIService } from '../openai/openai.service'; import { GoogleService } from '../google/google.service'; import { BytebotAgentModel, BytebotAgentService, BytebotAgentResponse, } from './agent.types'; import { AGENT_SYSTEM_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, } from './agent.constants'; import { SummariesService } from '../summaries/summaries.service'; import { handleComputerToolUse } from './agent.computer-use'; import { ProxyService } from '../proxy/proxy.service'; @Injectable() export class AgentProcessor { private readonly logger = new Logger(AgentProcessor.name); private currentTaskId: string | null = null; private isProcessing = false; private abortController: AbortController | null = null; private services: Record = {}; constructor( private readonly tasksService: TasksService, private readonly messagesService: MessagesService, private readonly summariesService: SummariesService, private readonly anthropicService: AnthropicService, private readonly openaiService: OpenAIService, private readonly googleService: GoogleService, private readonly proxyService: ProxyService, private readonly inputCaptureService: InputCaptureService, ) { this.services = { anthropic: this.anthropicService, openai: this.openaiService, google: this.googleService, proxy: this.proxyService, }; this.logger.log('AgentProcessor initialized'); } /** * Check if the processor is currently processing a task */ isRunning(): boolean { return this.isProcessing; } /** * Get the current task ID being processed */ getCurrentTaskId(): string | null { return this.currentTaskId; } @OnEvent('task.takeover') handleTaskTakeover({ taskId }: { taskId: string }) { this.logger.log(`Task takeover event received for task ID: ${taskId}`); // If the agent is still processing this task, abort any in-flight operations if (this.currentTaskId === taskId && this.isProcessing) { this.abortController?.abort(); } // Always start capturing user input so that emitted actions are received this.inputCaptureService.start(taskId); } @OnEvent('task.resume') handleTaskResume({ taskId }: { taskId: string }) { if (this.currentTaskId === taskId && this.isProcessing) { this.logger.log(`Task resume event received for task ID: ${taskId}`); this.abortController = new AbortController(); void this.runIteration(taskId); } } @OnEvent('task.cancel') async handleTaskCancel({ taskId }: { taskId: string }) { this.logger.log(`Task cancel event received for task ID: ${taskId}`); await this.stopProcessing(); } processTask(taskId: string) { this.logger.log(`Starting processing for task ID: ${taskId}`); if (this.isProcessing) { this.logger.warn('AgentProcessor is already processing another task'); return; } this.isProcessing = true; this.currentTaskId = taskId; this.abortController = new AbortController(); // Kick off the first iteration without blocking the caller void this.runIteration(taskId); } /** * Runs a single iteration of task processing and schedules the next * iteration via setImmediate while the task remains RUNNING. */ private async runIteration(taskId: string): Promise { if (!this.isProcessing) { return; } try { const task: Task = await this.tasksService.findById(taskId); if (task.status !== TaskStatus.RUNNING) { this.logger.log( `Task processing completed for task ID: ${taskId} with status: ${task.status}`, ); this.isProcessing = false; this.currentTaskId = null; return; } this.logger.log(`Processing iteration for task ID: ${taskId}`); // Refresh abort controller for this iteration to avoid accumulating // "abort" listeners on a single AbortSignal across iterations. this.abortController = new AbortController(); const latestSummary = await this.summariesService.findLatest(taskId); const unsummarizedMessages = await this.messagesService.findUnsummarized(taskId); const messages = [ ...(latestSummary ? [ { id: '', createdAt: new Date(), updatedAt: new Date(), taskId, summaryId: null, role: Role.USER, content: [ { type: MessageContentType.Text, text: latestSummary.content, }, ], }, ] : []), ...unsummarizedMessages, ]; this.logger.debug( `Sending ${messages.length} messages to LLM for processing`, ); const model = task.model as unknown as BytebotAgentModel; let agentResponse: BytebotAgentResponse; const service = this.services[model.provider]; if (!service) { this.logger.warn( `No service found for model provider: ${model.provider}`, ); await this.tasksService.update(taskId, { status: TaskStatus.FAILED, }); this.isProcessing = false; this.currentTaskId = null; return; } agentResponse = await service.generateMessage( AGENT_SYSTEM_PROMPT, messages, model.name, true, this.abortController.signal, ); const messageContentBlocks = agentResponse.contentBlocks; this.logger.debug( `Received ${messageContentBlocks.length} content blocks from LLM`, ); if (messageContentBlocks.length === 0) { this.logger.warn( `Task ID: ${taskId} received no content blocks from LLM, marking as failed`, ); await this.tasksService.update(taskId, { status: TaskStatus.FAILED, }); this.isProcessing = false; this.currentTaskId = null; return; } await this.messagesService.create({ content: messageContentBlocks, role: Role.ASSISTANT, taskId, }); // Calculate if we need to summarize based on token usage const contextWindow = model.contextWindow || 200000; // Default to 200k if not specified const contextThreshold = contextWindow * 0.75; const shouldSummarize = agentResponse.tokenUsage.totalTokens >= contextThreshold; if (shouldSummarize) { try { // After we've successfully generated a response, we can summarize the unsummarized messages const summaryResponse = await service.generateMessage( SUMMARIZATION_SYSTEM_PROMPT, [ ...messages, { id: '', createdAt: new Date(), updatedAt: new Date(), taskId, summaryId: null, role: Role.USER, content: [ { type: MessageContentType.Text, text: 'Respond with a summary of the messages above. Do not include any additional information.', }, ], }, ], model.name, false, this.abortController.signal, ); const summaryContentBlocks = summaryResponse.contentBlocks; this.logger.debug( `Received ${summaryContentBlocks.length} summary content blocks from LLM`, ); const summaryContent = summaryContentBlocks .filter( (block: MessageContentBlock) => block.type === MessageContentType.Text, ) .map((block: TextContentBlock) => block.text) .join('\n'); const summary = await this.summariesService.create({ content: summaryContent, taskId, }); await this.messagesService.attachSummary(taskId, summary.id, [ ...messages.map((message) => { return message.id; }), ]); this.logger.log( `Generated summary for task ${taskId} due to token usage (${agentResponse.tokenUsage.totalTokens}/${contextWindow})`, ); } catch (error: any) { this.logger.error( `Error summarizing messages for task ID: ${taskId}`, error.stack, ); } } this.logger.debug( `Token usage for task ${taskId}: ${agentResponse.tokenUsage.totalTokens}/${contextWindow} (${Math.round((agentResponse.tokenUsage.totalTokens / contextWindow) * 100)}%)`, ); const generatedToolResults: ToolResultContentBlock[] = []; let setTaskStatusToolUseBlock: SetTaskStatusToolUseBlock | null = null; for (const block of messageContentBlocks) { if (isComputerToolUseContentBlock(block)) { const result = await handleComputerToolUse(block, this.logger); generatedToolResults.push(result); } if (isCreateTaskToolUseBlock(block)) { const type = block.input.type?.toUpperCase() as TaskType; const priority = block.input.priority?.toUpperCase() as TaskPriority; await this.tasksService.create({ description: block.input.description, type, createdBy: Role.ASSISTANT, ...(block.input.scheduledFor && { scheduledFor: new Date(block.input.scheduledFor), }), model: task.model, priority, }); generatedToolResults.push({ type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'The task has been created', }, ], }); } if (isSetTaskStatusToolUseBlock(block)) { setTaskStatusToolUseBlock = block; generatedToolResults.push({ type: MessageContentType.ToolResult, tool_use_id: block.id, is_error: block.input.status === 'failed', content: [ { type: MessageContentType.Text, text: block.input.description, }, ], }); } } if (generatedToolResults.length > 0) { await this.messagesService.create({ content: generatedToolResults, role: Role.USER, taskId, }); } // Update the task status after all tool results have been generated if we have a set task status tool use block if (setTaskStatusToolUseBlock) { switch (setTaskStatusToolUseBlock.input.status) { case 'completed': await this.tasksService.update(taskId, { status: TaskStatus.COMPLETED, completedAt: new Date(), }); break; case 'needs_help': await this.tasksService.update(taskId, { status: TaskStatus.NEEDS_HELP, }); break; } } // Schedule the next iteration without blocking if (this.isProcessing) { setImmediate(() => this.runIteration(taskId)); } } catch (error: any) { if (error?.name === 'BytebotAgentInterrupt') { this.logger.warn(`Processing aborted for task ID: ${taskId}`); } else { this.logger.error( `Error during task processing iteration for task ID: ${taskId} - ${error.message}`, error.stack, ); await this.tasksService.update(taskId, { status: TaskStatus.FAILED, }); this.isProcessing = false; this.currentTaskId = null; } } } async stopProcessing(): Promise { if (!this.isProcessing) { return; } this.logger.log(`Stopping execution of task ${this.currentTaskId}`); // Signal any in-flight async operations to abort this.abortController?.abort(); await this.inputCaptureService.stop(); this.isProcessing = false; this.currentTaskId = null; } } ================================================ FILE: packages/bytebot-agent/src/agent/agent.scheduler.ts ================================================ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { TasksService } from '../tasks/tasks.service'; import { AgentProcessor } from './agent.processor'; import { TaskStatus } from '@prisma/client'; import { writeFile } from './agent.computer-use'; @Injectable() export class AgentScheduler implements OnModuleInit { private readonly logger = new Logger(AgentScheduler.name); constructor( private readonly tasksService: TasksService, private readonly agentProcessor: AgentProcessor, ) {} async onModuleInit() { this.logger.log('AgentScheduler initialized'); await this.handleCron(); } @Cron(CronExpression.EVERY_5_SECONDS) async handleCron() { const now = new Date(); const scheduledTasks = await this.tasksService.findScheduledTasks(); for (const scheduledTask of scheduledTasks) { if (scheduledTask.scheduledFor && scheduledTask.scheduledFor < now) { this.logger.debug( `Task ID: ${scheduledTask.id} is scheduled for ${scheduledTask.scheduledFor}, queuing it`, ); await this.tasksService.update(scheduledTask.id, { queuedAt: now, }); } } if (this.agentProcessor.isRunning()) { return; } // Find the highest priority task to execute const task = await this.tasksService.findNextTask(); if (task) { if (task.files.length > 0) { this.logger.debug( `Task ID: ${task.id} has files, writing them to the desktop`, ); for (const file of task.files) { await writeFile({ path: `/home/user/Desktop/${file.name}`, content: file.data, // file.data is already base64 encoded in the database }); } } await this.tasksService.update(task.id, { status: TaskStatus.RUNNING, executedAt: new Date(), }); this.logger.debug(`Processing task ID: ${task.id}`); this.agentProcessor.processTask(task.id); } } } ================================================ FILE: packages/bytebot-agent/src/agent/agent.tools.ts ================================================ /** * Common schema definitions for reuse */ const coordinateSchema = { type: 'object' as const, properties: { x: { type: 'number' as const, description: 'The x-coordinate', }, y: { type: 'number' as const, description: 'The y-coordinate', }, }, required: ['x', 'y'], }; const holdKeysSchema = { type: 'array' as const, items: { type: 'string' as const }, description: 'Optional array of keys to hold during the action', nullable: true, }; const buttonSchema = { type: 'string' as const, enum: ['left', 'right', 'middle'], description: 'The mouse button', }; /** * Tool definitions for mouse actions */ export const _moveMouseTool = { name: 'computer_move_mouse', description: 'Moves the mouse cursor to the specified coordinates', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Target coordinates for mouse movement', }, }, required: ['coordinates'], }, }; export const _traceMouseTool = { name: 'computer_trace_mouse', description: 'Moves the mouse cursor along a specified path of coordinates', input_schema: { type: 'object' as const, properties: { path: { type: 'array' as const, items: coordinateSchema, description: 'Array of coordinate objects representing the path', }, holdKeys: holdKeysSchema, }, required: ['path'], }, }; export const _clickMouseTool = { name: 'computer_click_mouse', description: 'Performs a mouse click at the specified coordinates or current position', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Optional click coordinates (defaults to current position)', nullable: true, }, button: buttonSchema, holdKeys: holdKeysSchema, clickCount: { type: 'integer' as const, description: 'Number of clicks to perform (e.g., 2 for double-click)', default: 1, }, }, required: ['button', 'clickCount'], }, }; export const _pressMouseTool = { name: 'computer_press_mouse', description: 'Presses or releases a specified mouse button', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Optional coordinates (defaults to current position)', nullable: true, }, button: buttonSchema, press: { type: 'string' as const, enum: ['up', 'down'], description: 'Whether to press down or release up', }, }, required: ['button', 'press'], }, }; export const _dragMouseTool = { name: 'computer_drag_mouse', description: 'Drags the mouse along a path while holding a button', input_schema: { type: 'object' as const, properties: { path: { type: 'array' as const, items: coordinateSchema, description: 'Array of coordinates representing the drag path', }, button: buttonSchema, holdKeys: holdKeysSchema, }, required: ['path', 'button'], }, }; export const _scrollTool = { name: 'computer_scroll', description: 'Scrolls the mouse wheel in the specified direction', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Coordinates where the scroll should occur', }, direction: { type: 'string' as const, enum: ['up', 'down', 'left', 'right'], description: 'The direction to scroll', }, scrollCount: { type: 'integer' as const, description: 'Number of scroll steps', }, holdKeys: holdKeysSchema, }, required: ['coordinates', 'direction', 'scrollCount'], }, }; /** * Tool definitions for keyboard actions */ export const _typeKeysTool = { name: 'computer_type_keys', description: 'Types a sequence of keys (useful for keyboard shortcuts)', input_schema: { type: 'object' as const, properties: { keys: { type: 'array' as const, items: { type: 'string' as const }, description: 'Array of key names to type in sequence', }, delay: { type: 'number' as const, description: 'Optional delay in milliseconds between key presses', nullable: true, }, }, required: ['keys'], }, }; export const _pressKeysTool = { name: 'computer_press_keys', description: 'Presses or releases specific keys (useful for holding modifiers)', input_schema: { type: 'object' as const, properties: { keys: { type: 'array' as const, items: { type: 'string' as const }, description: 'Array of key names to press or release', }, press: { type: 'string' as const, enum: ['up', 'down'], description: 'Whether to press down or release up', }, }, required: ['keys', 'press'], }, }; export const _typeTextTool = { name: 'computer_type_text', description: 'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.', input_schema: { type: 'object' as const, properties: { text: { type: 'string' as const, description: 'The text string to type', }, delay: { type: 'number' as const, description: 'Optional delay in milliseconds between characters', nullable: true, }, isSensitive: { type: 'boolean' as const, description: 'Flag to indicate sensitive information', nullable: true, }, }, required: ['text'], }, }; export const _pasteTextTool = { name: 'computer_paste_text', description: 'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.', input_schema: { type: 'object' as const, properties: { text: { type: 'string' as const, description: 'The text string to type', }, isSensitive: { type: 'boolean' as const, description: 'Flag to indicate sensitive information', nullable: true, }, }, required: ['text'], }, }; /** * Tool definitions for utility actions */ export const _waitTool = { name: 'computer_wait', description: 'Pauses execution for a specified duration', input_schema: { type: 'object' as const, properties: { duration: { type: 'integer' as const, enum: [500], description: 'The duration to wait in milliseconds', }, }, required: ['duration'], }, }; export const _screenshotTool = { name: 'computer_screenshot', description: 'Captures a screenshot of the current screen', input_schema: { type: 'object' as const, properties: {}, }, }; export const _cursorPositionTool = { name: 'computer_cursor_position', description: 'Gets the current (x, y) coordinates of the mouse cursor', input_schema: { type: 'object' as const, properties: {}, }, }; export const _applicationTool = { name: 'computer_application', description: 'Opens or focuses an application and ensures it is fullscreen', input_schema: { type: 'object' as const, properties: { application: { type: 'string' as const, enum: [ 'firefox', '1password', 'thunderbird', 'vscode', 'terminal', 'desktop', 'directory', ], description: 'The application to open or focus', }, }, required: ['application'], }, }; /** * Tool definitions for task management */ export const _setTaskStatusTool = { name: 'set_task_status', description: 'Sets the status of the current task', input_schema: { type: 'object' as const, properties: { status: { type: 'string' as const, enum: ['completed', 'needs_help'], description: 'The status of the task', }, description: { type: 'string' as const, description: 'If the task is completed, a summary of the task. If the task needs help, a description of the issue or clarification needed.', }, }, required: ['status', 'description'], }, }; export const _createTaskTool = { name: 'create_task', description: 'Creates a new task', input_schema: { type: 'object' as const, properties: { description: { type: 'string' as const, description: 'The description of the task', }, type: { type: 'string' as const, enum: ['IMMEDIATE', 'SCHEDULED'], description: 'The type of the task (defaults to IMMEDIATE)', }, scheduledFor: { type: 'string' as const, format: 'date-time', description: 'RFC 3339 / ISO 8601 datetime for scheduled tasks', }, priority: { type: 'string' as const, enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'], description: 'The priority of the task (defaults to MEDIUM)', }, }, required: ['description'], }, }; /** * Tool definition for reading files */ export const _readFileTool = { name: 'computer_read_file', description: 'Reads a file from the specified path and returns it as a document content block with base64 encoded data', input_schema: { type: 'object' as const, properties: { path: { type: 'string' as const, description: 'The file path to read from', }, }, required: ['path'], }, }; /** * Export all tools as an array */ export const agentTools = [ _moveMouseTool, _traceMouseTool, _clickMouseTool, _pressMouseTool, _dragMouseTool, _scrollTool, _typeKeysTool, _pressKeysTool, _typeTextTool, _pasteTextTool, _waitTool, _screenshotTool, _applicationTool, _cursorPositionTool, _setTaskStatusTool, _createTaskTool, _readFileTool, ]; ================================================ FILE: packages/bytebot-agent/src/agent/agent.types.ts ================================================ import { Message } from '@prisma/client'; import { MessageContentBlock } from '@bytebot/shared'; export interface BytebotAgentResponse { contentBlocks: MessageContentBlock[]; tokenUsage: { inputTokens: number; outputTokens: number; totalTokens: number; }; } export interface BytebotAgentService { generateMessage( systemPrompt: string, messages: Message[], model: string, useTools: boolean, signal?: AbortSignal, ): Promise; } export interface BytebotAgentModel { provider: 'anthropic' | 'openai' | 'google' | 'proxy'; name: string; title: string; contextWindow?: number; } export class BytebotAgentInterrupt extends Error { constructor() { super('BytebotAgentInterrupt'); this.name = 'BytebotAgentInterrupt'; } } ================================================ FILE: packages/bytebot-agent/src/agent/input-capture.service.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { io, Socket } from 'socket.io-client'; import { randomUUID } from 'crypto'; import { convertClickMouseActionToToolUseBlock, convertDragMouseActionToToolUseBlock, convertPressKeysActionToToolUseBlock, convertPressMouseActionToToolUseBlock, convertScrollActionToToolUseBlock, convertTypeKeysActionToToolUseBlock, convertTypeTextActionToToolUseBlock, ImageContentBlock, MessageContentBlock, MessageContentType, ScreenshotToolUseBlock, ToolResultContentBlock, UserActionContentBlock, } from '@bytebot/shared'; import { Role } from '@prisma/client'; import { MessagesService } from '../messages/messages.service'; import { ConfigService } from '@nestjs/config'; @Injectable() export class InputCaptureService { private readonly logger = new Logger(InputCaptureService.name); private socket: Socket | null = null; private capturing = false; constructor( private readonly messagesService: MessagesService, private readonly configService: ConfigService, ) {} isCapturing() { return this.capturing; } start(taskId: string) { if (this.socket?.connected && this.capturing) return; if (this.socket && !this.socket.connected) { this.socket.connect(); return; } const baseUrl = this.configService.get('BYTEBOT_DESKTOP_BASE_URL'); if (!baseUrl) { this.logger.warn('BYTEBOT_DESKTOP_BASE_URL missing.'); return; } this.socket = io(baseUrl, { transports: ['websocket'] }); this.socket.on('connect', () => { this.logger.log('Input socket connected'); this.capturing = true; }); this.socket.on( 'screenshotAndAction', async (shot: { image: string }, action: any) => { if (!this.capturing || !taskId) return; // The gateway only sends a click_mouse or drag_mouse action together with screenshots for now. if (action.action !== 'click_mouse' && action.action !== 'drag_mouse') return; const userActionBlock: UserActionContentBlock = { type: MessageContentType.UserAction, content: [ { type: MessageContentType.Image, source: { data: shot.image, media_type: 'image/png', type: 'base64', }, }, ], }; const toolUseId = randomUUID(); switch (action.action) { case 'drag_mouse': userActionBlock.content.push( convertDragMouseActionToToolUseBlock(action, toolUseId), ); break; case 'click_mouse': userActionBlock.content.push( convertClickMouseActionToToolUseBlock(action, toolUseId), ); break; } await this.messagesService.create({ content: [userActionBlock], role: Role.USER, taskId, }); }, ); this.socket.on('action', async (action: any) => { if (!this.capturing || !taskId) return; const toolUseId = randomUUID(); const userActionBlock: UserActionContentBlock = { type: MessageContentType.UserAction, content: [], }; switch (action.action) { case 'drag_mouse': userActionBlock.content.push( convertDragMouseActionToToolUseBlock(action, toolUseId), ); break; case 'press_mouse': userActionBlock.content.push( convertPressMouseActionToToolUseBlock(action, toolUseId), ); break; case 'type_keys': userActionBlock.content.push( convertTypeKeysActionToToolUseBlock(action, toolUseId), ); break; case 'press_keys': userActionBlock.content.push( convertPressKeysActionToToolUseBlock(action, toolUseId), ); break; case 'type_text': userActionBlock.content.push( convertTypeTextActionToToolUseBlock(action, toolUseId), ); break; case 'scroll': userActionBlock.content.push( convertScrollActionToToolUseBlock(action, toolUseId), ); break; default: this.logger.warn(`Unknown action ${action.action}`); } if (userActionBlock.content.length > 0) { await this.messagesService.create({ content: [userActionBlock], role: Role.USER, taskId, }); } }); this.socket.on('disconnect', () => { this.logger.log('Input socket disconnected'); this.capturing = false; }); } async stop() { if (!this.socket) return; if (this.socket.connected) this.socket.disconnect(); else this.socket.removeAllListeners(); this.socket = null; this.capturing = false; } } ================================================ FILE: packages/bytebot-agent/src/anthropic/anthropic.constants.ts ================================================ import { BytebotAgentModel } from '../agent/agent.types'; export const ANTHROPIC_MODELS: BytebotAgentModel[] = [ { provider: 'anthropic', name: 'claude-opus-4-1-20250805', title: 'Claude Opus 4.1', contextWindow: 200000, }, { provider: 'anthropic', name: 'claude-sonnet-4-20250514', title: 'Claude Sonnet 4', contextWindow: 200000, }, ]; export const DEFAULT_MODEL = ANTHROPIC_MODELS[0]; ================================================ FILE: packages/bytebot-agent/src/anthropic/anthropic.module.ts ================================================ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AnthropicService } from './anthropic.service'; @Module({ imports: [ConfigModule], providers: [AnthropicService], exports: [AnthropicService], }) export class AnthropicModule {} ================================================ FILE: packages/bytebot-agent/src/anthropic/anthropic.service.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Anthropic, { APIUserAbortError } from '@anthropic-ai/sdk'; import { MessageContentBlock, MessageContentType, TextContentBlock, ToolUseContentBlock, ThinkingContentBlock, RedactedThinkingContentBlock, isUserActionContentBlock, isComputerToolUseContentBlock, } from '@bytebot/shared'; import { DEFAULT_MODEL } from './anthropic.constants'; import { Message, Role } from '@prisma/client'; import { anthropicTools } from './anthropic.tools'; import { BytebotAgentService, BytebotAgentInterrupt, BytebotAgentResponse, } from '../agent/agent.types'; @Injectable() export class AnthropicService implements BytebotAgentService { private readonly anthropic: Anthropic; private readonly logger = new Logger(AnthropicService.name); constructor(private readonly configService: ConfigService) { const apiKey = this.configService.get('ANTHROPIC_API_KEY'); if (!apiKey) { this.logger.warn( 'ANTHROPIC_API_KEY is not set. AnthropicService will not work properly.', ); } this.anthropic = new Anthropic({ apiKey: apiKey || 'dummy-key-for-initialization', }); } async generateMessage( systemPrompt: string, messages: Message[], model: string = DEFAULT_MODEL.name, useTools: boolean = true, signal?: AbortSignal, ): Promise { try { const maxTokens = 8192; // Convert our message content blocks to Anthropic's expected format const anthropicMessages = this.formatMessagesForAnthropic(messages); // add cache_control to last tool anthropicTools[anthropicTools.length - 1].cache_control = { type: 'ephemeral', }; // Make the API call const response = await this.anthropic.messages.create( { model, max_tokens: maxTokens * 2, thinking: { type: 'disabled' }, system: [ { type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' }, }, ], messages: anthropicMessages, tools: useTools ? anthropicTools : [], }, { signal }, ); // Convert Anthropic's response to our message content blocks format return { contentBlocks: this.formatAnthropicResponse(response.content), tokenUsage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, totalTokens: response.usage.input_tokens + response.usage.output_tokens, }, }; } catch (error) { this.logger.log(error); if (error instanceof APIUserAbortError) { this.logger.log('Anthropic API call aborted'); throw new BytebotAgentInterrupt(); } this.logger.error( `Error sending message to Anthropic: ${error.message}`, error.stack, ); throw error; } } /** * Convert our MessageContentBlock format to Anthropic's message format */ private formatMessagesForAnthropic( messages: Message[], ): Anthropic.MessageParam[] { const anthropicMessages: Anthropic.MessageParam[] = []; // Process each message content block for (const [index, message] of messages.entries()) { const messageContentBlocks = message.content as MessageContentBlock[]; const content: Anthropic.ContentBlockParam[] = []; if ( messageContentBlocks.every((block) => isUserActionContentBlock(block)) ) { const userActionContentBlocks = messageContentBlocks.flatMap( (block) => block.content, ); for (const block of userActionContentBlocks) { if (isComputerToolUseContentBlock(block)) { content.push({ type: 'text', text: `User performed action: ${block.name}\n${JSON.stringify(block.input, null, 2)}`, }); } else { content.push(block as Anthropic.ContentBlockParam); } } } else { content.push( ...messageContentBlocks.map( (block) => block as Anthropic.ContentBlockParam, ), ); } if (index === messages.length - 1) { content[content.length - 1]['cache_control'] = { type: 'ephemeral', }; } anthropicMessages.push({ role: message.role === Role.USER ? 'user' : 'assistant', content: content, }); } return anthropicMessages; } /** * Convert Anthropic's response content to our MessageContentBlock format */ private formatAnthropicResponse( content: Anthropic.ContentBlock[], ): MessageContentBlock[] { return content.map((block) => { switch (block.type) { case 'text': return { type: MessageContentType.Text, text: block.text, } as TextContentBlock; case 'tool_use': return { type: MessageContentType.ToolUse, id: block.id, name: block.name, input: block.input, } as ToolUseContentBlock; case 'thinking': return { type: MessageContentType.Thinking, thinking: block.thinking, signature: block.signature, } as ThinkingContentBlock; case 'redacted_thinking': return { type: MessageContentType.RedactedThinking, data: block.data, } as RedactedThinkingContentBlock; } }); } } ================================================ FILE: packages/bytebot-agent/src/anthropic/anthropic.tools.ts ================================================ import Anthropic from '@anthropic-ai/sdk'; import { agentTools } from '../agent/agent.tools'; /** * Converts an agent tool definition to an Anthropic.Tool */ function agentToolToAnthropicTool(agentTool: any): Anthropic.Tool { return agentTool as Anthropic.Tool; } /** * Creates a mapped object of tools by name */ const toolMap = agentTools.reduce( (acc, tool) => { const anthropicTool = agentToolToAnthropicTool(tool); const camelCaseName = tool.name .split('_') .map((part, index) => { if (index === 0) return part; if (part === 'computer') return ''; return part.charAt(0).toUpperCase() + part.slice(1); }) .join('') .replace(/^computer/, ''); acc[camelCaseName + 'Tool'] = anthropicTool; return acc; }, {} as Record, ); // Export individual tools with proper names export const moveMouseTool = toolMap.moveMouseTool; export const traceMouseTool = toolMap.traceMouseTool; export const clickMouseTool = toolMap.clickMouseTool; export const pressMouseTool = toolMap.pressMouseTool; export const dragMouseTool = toolMap.dragMouseTool; export const scrollTool = toolMap.scrollTool; export const typeKeysTool = toolMap.typeKeysTool; export const pressKeysTool = toolMap.pressKeysTool; export const typeTextTool = toolMap.typeTextTool; export const pasteTextTool = toolMap.pasteTextTool; export const waitTool = toolMap.waitTool; export const screenshotTool = toolMap.screenshotTool; export const cursorPositionTool = toolMap.cursorPositionTool; export const setTaskStatusTool = toolMap.setTaskStatusTool; export const createTaskTool = toolMap.createTaskTool; export const applicationTool = toolMap.applicationTool; // Array of all tools export const anthropicTools: Anthropic.Tool[] = agentTools.map( agentToolToAnthropicTool, ); ================================================ FILE: packages/bytebot-agent/src/app.controller.ts ================================================ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } } ================================================ FILE: packages/bytebot-agent/src/app.module.ts ================================================ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AgentModule } from './agent/agent.module'; import { TasksModule } from './tasks/tasks.module'; import { MessagesModule } from './messages/messages.module'; import { AnthropicModule } from './anthropic/anthropic.module'; import { OpenAIModule } from './openai/openai.module'; import { GoogleModule } from './google/google.module'; import { PrismaModule } from './prisma/prisma.module'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { SummariesModule } from './summaries/summaries.modue'; import { ProxyModule } from './proxy/proxy.module'; @Module({ imports: [ ScheduleModule.forRoot(), EventEmitterModule.forRoot(), ConfigModule.forRoot({ isGlobal: true, }), AgentModule, TasksModule, MessagesModule, SummariesModule, AnthropicModule, OpenAIModule, GoogleModule, ProxyModule, PrismaModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {} ================================================ FILE: packages/bytebot-agent/src/app.service.ts ================================================ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } } ================================================ FILE: packages/bytebot-agent/src/google/google.constants.ts ================================================ import { BytebotAgentModel } from '../agent/agent.types'; export const GOOGLE_MODELS: BytebotAgentModel[] = [ { provider: 'google', name: 'gemini-2.5-pro', title: 'Gemini 2.5 Pro', contextWindow: 1000000, }, { provider: 'google', name: 'gemini-2.5-flash', title: 'Gemini 2.5 Flash', contextWindow: 1000000, }, ]; export const DEFAULT_MODEL = GOOGLE_MODELS[0]; ================================================ FILE: packages/bytebot-agent/src/google/google.module.ts ================================================ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GoogleService } from './google.service'; @Module({ imports: [ConfigModule], providers: [GoogleService], exports: [GoogleService], }) export class GoogleModule {} ================================================ FILE: packages/bytebot-agent/src/google/google.service.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { isComputerToolUseContentBlock, isImageContentBlock, isUserActionContentBlock, MessageContentBlock, MessageContentType, TextContentBlock, ThinkingContentBlock, ToolUseContentBlock, } from '@bytebot/shared'; import { BytebotAgentService, BytebotAgentInterrupt, BytebotAgentResponse, } from '../agent/agent.types'; import { Message, Role } from '@prisma/client'; import { googleTools } from './google.tools'; import { Content, GenerateContentResponse, GoogleGenAI, Part, } from '@google/genai'; import { v4 as uuid } from 'uuid'; import { DEFAULT_MODEL } from './google.constants'; @Injectable() export class GoogleService implements BytebotAgentService { private readonly google: GoogleGenAI; private readonly logger = new Logger(GoogleService.name); constructor(private readonly configService: ConfigService) { const apiKey = this.configService.get('GEMINI_API_KEY'); if (!apiKey) { this.logger.warn( 'GEMINI_API_KEY is not set. GoogleService will not work properly.', ); } this.google = new GoogleGenAI({ apiKey: apiKey || 'dummy-key-for-initialization', }); } async generateMessage( systemPrompt: string, messages: Message[], model: string = DEFAULT_MODEL.name, useTools: boolean = true, signal?: AbortSignal, ): Promise { try { const maxTokens = 8192; // Convert our message content blocks to Anthropic's expected format const googleMessages = this.formatMessagesForGoogle(messages); const response: GenerateContentResponse = await this.google.models.generateContent({ model, contents: googleMessages, config: { thinkingConfig: { thinkingBudget: 24576, }, maxOutputTokens: maxTokens, systemInstruction: systemPrompt, tools: useTools ? [ { functionDeclarations: googleTools, }, ] : [], abortSignal: signal, }, }); const candidate = response.candidates?.[0]; if (!candidate) { throw new Error('No candidate found in response'); } const content = candidate.content; if (!content) { throw new Error('No content found in candidate'); } if (!content.parts) { throw new Error('No parts found in content'); } return { contentBlocks: this.formatGoogleResponse(content.parts), tokenUsage: { inputTokens: response.usageMetadata?.promptTokenCount || 0, outputTokens: response.usageMetadata?.candidatesTokenCount || 0, totalTokens: response.usageMetadata?.totalTokenCount || 0, }, }; } catch (error) { if (error.message.includes('AbortError')) { throw new BytebotAgentInterrupt(); } this.logger.error( `Error sending message to Google Gemini: ${error.message}`, error.stack, ); throw error; } } /** * Convert our MessageContentBlock format to Google Gemini's message format */ private formatMessagesForGoogle(messages: Message[]): Content[] { const googleMessages: Content[] = []; // Process each message content block for (const message of messages) { const messageContentBlocks = message.content as MessageContentBlock[]; const parts: Part[] = []; if ( messageContentBlocks.every((block) => isUserActionContentBlock(block)) ) { const userActionContentBlocks = messageContentBlocks.flatMap( (block) => block.content, ); for (const block of userActionContentBlocks) { if (isComputerToolUseContentBlock(block)) { parts.push({ text: `User performed action: ${block.name}\n${JSON.stringify(block.input, null, 2)}`, }); } else if (isImageContentBlock(block)) { parts.push({ inlineData: { data: block.source.data, mimeType: block.source.media_type, }, }); } } } else { for (const block of messageContentBlocks) { switch (block.type) { case MessageContentType.Text: parts.push({ text: block.text, }); break; case MessageContentType.ToolUse: parts.push({ functionCall: { id: block.id, name: block.name, args: block.input, }, }); break; case MessageContentType.Image: parts.push({ inlineData: { data: block.source.data, mimeType: block.source.media_type, }, }); break; case MessageContentType.ToolResult: { const toolResultContentBlock = block.content[0]; if (toolResultContentBlock.type === MessageContentType.Image) { parts.push({ functionResponse: { id: block.tool_use_id, name: 'screenshot', response: { ...(!block.is_error && { output: 'screenshot successful', }), ...(block.is_error && { error: block.content[0] }), }, }, }); parts.push({ inlineData: { data: toolResultContentBlock.source.data, mimeType: toolResultContentBlock.source.media_type, }, }); break; } parts.push({ functionResponse: { id: block.tool_use_id, name: this.getToolName(block.tool_use_id, messages), response: { ...(!block.is_error && { output: block.content[0] }), ...(block.is_error && { error: block.content[0] }), }, }, }); break; } case MessageContentType.Thinking: parts.push({ text: block.thinking, thoughtSignature: block.signature, thought: true, }); break; default: parts.push({ text: JSON.stringify(block), }); break; } } } googleMessages.push({ role: message.role === Role.USER ? 'user' : 'model', parts: parts, }); } return googleMessages; } // Find the content block with the tool_use_id and return the name private getToolName( tool_use_id: string, messages: Message[], ): string | undefined { const toolMessage = messages.find((message) => (message.content as MessageContentBlock[]).some( (block) => block.type === MessageContentType.ToolUse && block.id === tool_use_id, ), ); if (!toolMessage) { return undefined; } const toolBlock = (toolMessage.content as MessageContentBlock[]).find( (block) => block.type === MessageContentType.ToolUse && block.id === tool_use_id, ); if (!toolBlock) { return undefined; } return (toolBlock as ToolUseContentBlock).name; } /** * Convert Google Gemini's response content to our MessageContentBlock format */ private formatGoogleResponse(parts: Part[]): MessageContentBlock[] { return parts.map((part) => { if (part.text) { return { type: MessageContentType.Text, text: part.text, } as TextContentBlock; } if (part.thought) { return { type: MessageContentType.Thinking, signature: part.thoughtSignature, thinking: part.text, } as ThinkingContentBlock; } if (part.functionCall) { return { type: MessageContentType.ToolUse, id: part.functionCall.id || uuid(), name: part.functionCall.name, input: part.functionCall.args, } as ToolUseContentBlock; } this.logger.warn(`Unknown content type from Google: ${part}`); return { type: MessageContentType.Text, text: JSON.stringify(part), } as TextContentBlock; }); } } ================================================ FILE: packages/bytebot-agent/src/google/google.tools.ts ================================================ import { FunctionDeclaration, Type } from '@google/genai'; import { agentTools } from '../agent/agent.tools'; /** * Converts JSON Schema type to Google Genai Type */ function jsonSchemaTypeToGoogleType(type: string): Type { switch (type) { case 'string': return Type.STRING; case 'number': return Type.NUMBER; case 'integer': return Type.INTEGER; case 'boolean': return Type.BOOLEAN; case 'array': return Type.ARRAY; case 'object': return Type.OBJECT; default: return Type.STRING; } } /** * Converts JSON Schema to Google Genai parameter schema */ function convertJsonSchemaToGoogleSchema(schema: any): any { if (!schema) return {}; const result: any = { type: jsonSchemaTypeToGoogleType(schema.type), }; if (schema.description) { result.description = schema.description; } // Only include enum if the property type is string; otherwise it is invalid for Google GenAI if (schema.type === 'string' && schema.enum && Array.isArray(schema.enum)) { result.enum = schema.enum; } if (schema.nullable) { result.nullable = true; } if (schema.type === 'array' && schema.items) { result.items = convertJsonSchemaToGoogleSchema(schema.items); } if (schema.type === 'object' && schema.properties) { result.properties = {}; for (const [key, value] of Object.entries(schema.properties)) { result.properties[key] = convertJsonSchemaToGoogleSchema(value); } if (schema.required) { result.required = schema.required; } } return result; } /** * Converts an agent tool definition to a Google FunctionDeclaration */ function agentToolToGoogleTool(agentTool: any): FunctionDeclaration { const parameters = convertJsonSchemaToGoogleSchema(agentTool.input_schema); return { name: agentTool.name, description: agentTool.description, parameters, }; } /** * Creates a mapped object of tools by name */ const toolMap = agentTools.reduce( (acc, tool) => { const googleTool = agentToolToGoogleTool(tool); const camelCaseName = tool.name .split('_') .map((part, index) => { if (index === 0) return part; if (part === 'computer') return ''; return part.charAt(0).toUpperCase() + part.slice(1); }) .join('') .replace(/^computer/, ''); acc[camelCaseName + 'Tool'] = googleTool; return acc; }, {} as Record, ); // Export individual tools with proper names export const moveMouseTool = toolMap.moveMouseTool; export const traceMouseTool = toolMap.traceMouseTool; export const clickMouseTool = toolMap.clickMouseTool; export const pressMouseTool = toolMap.pressMouseTool; export const dragMouseTool = toolMap.dragMouseTool; export const scrollTool = toolMap.scrollTool; export const typeKeysTool = toolMap.typeKeysTool; export const pressKeysTool = toolMap.pressKeysTool; export const typeTextTool = toolMap.typeTextTool; export const pasteTextTool = toolMap.pasteTextTool; export const waitTool = toolMap.waitTool; export const screenshotTool = toolMap.screenshotTool; export const cursorPositionTool = toolMap.cursorPositionTool; export const setTaskStatusTool = toolMap.setTaskStatusTool; export const createTaskTool = toolMap.createTaskTool; export const applicationTool = toolMap.applicationTool; // Array of all tools export const googleTools: FunctionDeclaration[] = agentTools.map( agentToolToGoogleTool, ); ================================================ FILE: packages/bytebot-agent/src/main.ts ================================================ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { webcrypto } from 'crypto'; import { json, urlencoded } from 'express'; // Polyfill for crypto global (required by @nestjs/schedule) if (!globalThis.crypto) { globalThis.crypto = webcrypto as any; } async function bootstrap() { console.log('Starting bytebot-agent application...'); try { const app = await NestFactory.create(AppModule); // Configure body parser with increased payload size limit (50MB) app.use(json({ limit: '50mb' })); app.use(urlencoded({ limit: '50mb', extended: true })); // Enable CORS app.enableCors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], }); await app.listen(process.env.PORT ?? 9991); } catch (error) { console.error('Error starting application:', error); } } bootstrap(); ================================================ FILE: packages/bytebot-agent/src/messages/messages.module.ts ================================================ import { Module, forwardRef } from '@nestjs/common'; import { MessagesService } from './messages.service'; import { PrismaModule } from '../prisma/prisma.module'; import { TasksModule } from '../tasks/tasks.module'; @Module({ imports: [PrismaModule, forwardRef(() => TasksModule)], providers: [MessagesService], exports: [MessagesService], }) export class MessagesModule {} ================================================ FILE: packages/bytebot-agent/src/messages/messages.service.ts ================================================ import { Injectable, NotFoundException, Inject, forwardRef, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { Message, Role, Prisma } from '@prisma/client'; import { MessageContentBlock, isComputerToolUseContentBlock, isToolResultContentBlock, isUserActionContentBlock, } from '@bytebot/shared'; import { TasksGateway } from '../tasks/tasks.gateway'; // Extended message type for processing export interface ProcessedMessage extends Message { take_over?: boolean; } export interface GroupedMessages { role: Role; messages: ProcessedMessage[]; take_over?: boolean; } @Injectable() export class MessagesService { constructor( private prisma: PrismaService, @Inject(forwardRef(() => TasksGateway)) private readonly tasksGateway: TasksGateway, ) {} async create(data: { content: MessageContentBlock[]; role: Role; taskId: string; }): Promise { const message = await this.prisma.message.create({ data: { content: data.content as Prisma.InputJsonValue, role: data.role, taskId: data.taskId, }, }); this.tasksGateway.emitNewMessage(data.taskId, message); return message; } async findEvery(taskId: string): Promise { return this.prisma.message.findMany({ where: { taskId, }, orderBy: { createdAt: 'asc', }, }); } async findAll( taskId: string, options?: { limit?: number; page?: number; }, ): Promise { const { limit = 10, page = 1 } = options || {}; // Calculate offset based on page and limit const offset = (page - 1) * limit; return this.prisma.message.findMany({ where: { taskId, }, orderBy: { createdAt: 'asc', }, take: limit, skip: offset, }); } async findUnsummarized(taskId: string): Promise { return this.prisma.message.findMany({ where: { taskId, // find messages that don't have a summaryId summaryId: null, }, orderBy: { createdAt: 'asc' }, }); } async attachSummary( taskId: string, summaryId: string, messageIds: string[], ): Promise { if (messageIds.length === 0) { return; } await this.prisma.message.updateMany({ where: { taskId, id: { in: messageIds } }, data: { summaryId }, }); } /** * Groups back-to-back messages from the same role and take_over status */ private groupBackToBackMessages( messages: ProcessedMessage[], ): GroupedMessages[] { const groupedConversation: GroupedMessages[] = []; let currentGroup: GroupedMessages | null = null; for (const message of messages) { const role = message.role; const isTakeOver = message.take_over || false; // If this is the first message, role is different, or take_over status is different from the previous group if ( !currentGroup || currentGroup.role !== role || currentGroup.take_over !== isTakeOver ) { // Save the previous group if it exists if (currentGroup) { groupedConversation.push(currentGroup); } // Start a new group currentGroup = { role: role, messages: [message], take_over: isTakeOver, }; } else { // Same role and take_over status as previous, merge the content currentGroup.messages.push(message); } } // Add the last group if (currentGroup) { groupedConversation.push(currentGroup); } return groupedConversation; } /** * Filters and processes messages, adding take_over flags where appropriate * Only text messages from the user should appear as user messages * Computer tool use messages should be shown as assistant messages with take_over flag */ private filterMessages(messages: Message[]): ProcessedMessage[] { const filteredMessages: ProcessedMessage[] = []; for (const message of messages) { const processedMessage: ProcessedMessage = { ...message }; const contentBlocks = message.content as MessageContentBlock[]; // If the role is a user message and all the content blocks are tool result blocks or they are take over actions if (message.role === Role.USER) { if (contentBlocks.every((block) => isToolResultContentBlock(block))) { // Pure tool results should be shown as assistant messages processedMessage.role = Role.ASSISTANT; } else if ( contentBlocks.every((block) => isUserActionContentBlock(block)) ) { // Extract computer tool use (take over actions) from the user action content blocks and show them as assistant messages with take_over flag processedMessage.content = contentBlocks .flatMap((block) => { return block.content; }) .filter((block) => isComputerToolUseContentBlock(block)); processedMessage.role = Role.ASSISTANT; processedMessage.take_over = true; } // If there are text blocks mixed with tool blocks, keep as user message // Only pure text messages from user should remain as user messages } filteredMessages.push(processedMessage); } return filteredMessages; } /** * Returns raw messages without any processing */ async findRawMessages( taskId: string, options?: { limit?: number; page?: number; }, ): Promise { return this.findAll(taskId, options); } /** * Returns processed and grouped messages for the chat UI */ async findProcessedMessages( taskId: string, options?: { limit?: number; page?: number; }, ): Promise { const messages = await this.findAll(taskId, options); const filteredMessages = this.filterMessages(messages); return this.groupBackToBackMessages(filteredMessages); } } ================================================ FILE: packages/bytebot-agent/src/openai/openai.constants.ts ================================================ import { BytebotAgentModel } from 'src/agent/agent.types'; export const OPENAI_MODELS: BytebotAgentModel[] = [ { provider: 'openai', name: 'o3-2025-04-16', title: 'o3', contextWindow: 200000, }, { provider: 'openai', name: 'gpt-4.1-2025-04-14', title: 'GPT-4.1', contextWindow: 1047576, }, ]; export const DEFAULT_MODEL = OPENAI_MODELS[0]; ================================================ FILE: packages/bytebot-agent/src/openai/openai.module.ts ================================================ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { OpenAIService } from './openai.service'; @Module({ imports: [ConfigModule], providers: [OpenAIService], exports: [OpenAIService], }) export class OpenAIModule {} ================================================ FILE: packages/bytebot-agent/src/openai/openai.service.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import OpenAI, { APIUserAbortError } from 'openai'; import { MessageContentBlock, MessageContentType, TextContentBlock, ToolUseContentBlock, ToolResultContentBlock, ThinkingContentBlock, isUserActionContentBlock, isComputerToolUseContentBlock, isImageContentBlock, } from '@bytebot/shared'; import { DEFAULT_MODEL } from './openai.constants'; import { Message, Role } from '@prisma/client'; import { openaiTools } from './openai.tools'; import { BytebotAgentService, BytebotAgentInterrupt, BytebotAgentResponse, } from '../agent/agent.types'; @Injectable() export class OpenAIService implements BytebotAgentService { private readonly openai: OpenAI; private readonly logger = new Logger(OpenAIService.name); constructor(private readonly configService: ConfigService) { const apiKey = this.configService.get('OPENAI_API_KEY'); if (!apiKey) { this.logger.warn( 'OPENAI_API_KEY is not set. OpenAIService will not work properly.', ); } this.openai = new OpenAI({ apiKey: apiKey || 'dummy-key-for-initialization', }); } async generateMessage( systemPrompt: string, messages: Message[], model: string = DEFAULT_MODEL.name, useTools: boolean = true, signal?: AbortSignal, ): Promise { const isReasoning = model.startsWith('o'); try { const openaiMessages = this.formatMessagesForOpenAI(messages); const maxTokens = 8192; const response = await this.openai.responses.create( { model, max_output_tokens: maxTokens, input: openaiMessages, instructions: systemPrompt, tools: useTools ? openaiTools : [], reasoning: isReasoning ? { effort: 'medium' } : null, store: false, include: isReasoning ? ['reasoning.encrypted_content'] : [], }, { signal }, ); return { contentBlocks: this.formatOpenAIResponse(response.output), tokenUsage: { inputTokens: response.usage?.input_tokens || 0, outputTokens: response.usage?.output_tokens || 0, totalTokens: response.usage?.total_tokens || 0, }, }; } catch (error: any) { console.log('error', error); console.log('error name', error.name); if (error instanceof APIUserAbortError) { this.logger.log('OpenAI API call aborted'); throw new BytebotAgentInterrupt(); } this.logger.error( `Error sending message to OpenAI: ${error.message}`, error.stack, ); throw error; } } private formatMessagesForOpenAI( messages: Message[], ): OpenAI.Responses.ResponseInputItem[] { const openaiMessages: OpenAI.Responses.ResponseInputItem[] = []; for (const message of messages) { const messageContentBlocks = message.content as MessageContentBlock[]; if ( messageContentBlocks.every((block) => isUserActionContentBlock(block)) ) { const userActionContentBlocks = messageContentBlocks.flatMap( (block) => block.content, ); for (const block of userActionContentBlocks) { if (isComputerToolUseContentBlock(block)) { openaiMessages.push({ type: 'message', role: 'user', content: [ { type: 'input_text', text: `User performed action: ${block.name}\n${JSON.stringify(block.input, null, 2)}`, }, ], }); } else if (isImageContentBlock(block)) { openaiMessages.push({ role: 'user', type: 'message', content: [ { type: 'input_image', detail: 'high', image_url: `data:${block.source.media_type};base64,${block.source.data}`, }, ], } as OpenAI.Responses.ResponseInputItem.Message); } } } else { // Convert content blocks to OpenAI format for (const block of messageContentBlocks) { switch (block.type) { case MessageContentType.Text: { if (message.role === Role.USER) { openaiMessages.push({ type: 'message', role: 'user', content: [ { type: 'input_text', text: block.text, }, ], } as OpenAI.Responses.ResponseInputItem.Message); } else { openaiMessages.push({ type: 'message', role: 'assistant', content: [ { type: 'output_text', text: block.text, }, ], } as OpenAI.Responses.ResponseOutputMessage); } break; } case MessageContentType.ToolUse: // For assistant messages with tool use, convert to function call if (message.role === Role.ASSISTANT) { const toolBlock = block as ToolUseContentBlock; openaiMessages.push({ type: 'function_call', call_id: toolBlock.id, name: toolBlock.name, arguments: JSON.stringify(toolBlock.input), } as OpenAI.Responses.ResponseFunctionToolCall); } break; case MessageContentType.Thinking: { const thinkingBlock = block; openaiMessages.push({ type: 'reasoning', id: thinkingBlock.signature, encrypted_content: thinkingBlock.thinking, summary: [], } as OpenAI.Responses.ResponseReasoningItem); break; } case MessageContentType.ToolResult: { // Handle tool results as function call outputs const toolResult = block; // Tool results should be added as separate items in the response toolResult.content.forEach((content) => { if (content.type === MessageContentType.Text) { openaiMessages.push({ type: 'function_call_output', call_id: toolResult.tool_use_id, output: content.text, } as OpenAI.Responses.ResponseInputItem.FunctionCallOutput); } if (content.type === MessageContentType.Image) { openaiMessages.push({ type: 'function_call_output', call_id: toolResult.tool_use_id, output: 'screenshot', } as OpenAI.Responses.ResponseInputItem.FunctionCallOutput); openaiMessages.push({ role: 'user', type: 'message', content: [ { type: 'input_image', detail: 'high', image_url: `data:${content.source.media_type};base64,${content.source.data}`, }, ], } as OpenAI.Responses.ResponseInputItem.Message); } }); break; } default: // Handle unknown content types as text openaiMessages.push({ role: 'user', type: 'message', content: [ { type: 'input_text', text: JSON.stringify(block), }, ], } as OpenAI.Responses.ResponseInputItem.Message); } } } } return openaiMessages; } private formatOpenAIResponse( response: OpenAI.Responses.ResponseOutputItem[], ): MessageContentBlock[] { const contentBlocks: MessageContentBlock[] = []; for (const item of response) { // Check the type of the output item switch (item.type) { case 'message': // Handle ResponseOutputMessage const message = item; for (const content of message.content) { if ('text' in content) { // ResponseOutputText contentBlocks.push({ type: MessageContentType.Text, text: content.text, } as TextContentBlock); } else if ('refusal' in content) { // ResponseOutputRefusal contentBlocks.push({ type: MessageContentType.Text, text: `Refusal: ${content.refusal}`, } as TextContentBlock); } } break; case 'function_call': // Handle ResponseFunctionToolCall const toolCall = item; contentBlocks.push({ type: MessageContentType.ToolUse, id: toolCall.call_id, name: toolCall.name, input: JSON.parse(toolCall.arguments), } as ToolUseContentBlock); break; case 'file_search_call': case 'web_search_call': case 'computer_call': case 'reasoning': const reasoning = item as OpenAI.Responses.ResponseReasoningItem; if (reasoning.encrypted_content) { contentBlocks.push({ type: MessageContentType.Thinking, thinking: reasoning.encrypted_content, signature: reasoning.id, } as ThinkingContentBlock); } break; case 'image_generation_call': case 'code_interpreter_call': case 'local_shell_call': case 'mcp_call': case 'mcp_list_tools': case 'mcp_approval_request': // Handle other tool types as text for now this.logger.warn( `Unsupported response output item type: ${item.type}`, ); contentBlocks.push({ type: MessageContentType.Text, text: JSON.stringify(item), } as TextContentBlock); break; default: // Handle unknown types this.logger.warn( `Unknown response output item type: ${JSON.stringify(item)}`, ); contentBlocks.push({ type: MessageContentType.Text, text: JSON.stringify(item), } as TextContentBlock); } } return contentBlocks; } } ================================================ FILE: packages/bytebot-agent/src/openai/openai.tools.ts ================================================ import OpenAI from 'openai'; import { agentTools } from '../agent/agent.tools'; function agentToolToOpenAITool(agentTool: any): OpenAI.Responses.FunctionTool { return { type: 'function', name: agentTool.name, description: agentTool.description, parameters: agentTool.input_schema, } as OpenAI.Responses.FunctionTool; } /** * Creates a mapped object of tools by name */ const toolMap = agentTools.reduce( (acc, tool) => { const anthropicTool = agentToolToOpenAITool(tool); const camelCaseName = tool.name .split('_') .map((part, index) => { if (index === 0) return part; if (part === 'computer') return ''; return part.charAt(0).toUpperCase() + part.slice(1); }) .join('') .replace(/^computer/, ''); acc[camelCaseName + 'Tool'] = anthropicTool; return acc; }, {} as Record, ); // Export individual tools with proper names export const moveMouseTool = toolMap.moveMouseTool; export const traceMouseTool = toolMap.traceMouseTool; export const clickMouseTool = toolMap.clickMouseTool; export const pressMouseTool = toolMap.pressMouseTool; export const dragMouseTool = toolMap.dragMouseTool; export const scrollTool = toolMap.scrollTool; export const typeKeysTool = toolMap.typeKeysTool; export const pressKeysTool = toolMap.pressKeysTool; export const typeTextTool = toolMap.typeTextTool; export const pasteTextTool = toolMap.pasteTextTool; export const waitTool = toolMap.waitTool; export const screenshotTool = toolMap.screenshotTool; export const cursorPositionTool = toolMap.cursorPositionTool; export const setTaskStatusTool = toolMap.setTaskStatusTool; export const createTaskTool = toolMap.createTaskTool; export const applicationTool = toolMap.applicationTool; // Array of all tools export const openaiTools: OpenAI.Responses.FunctionTool[] = agentTools.map( agentToolToOpenAITool, ); ================================================ FILE: packages/bytebot-agent/src/prisma/prisma.module.ts ================================================ import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ================================================ FILE: packages/bytebot-agent/src/prisma/prisma.service.ts ================================================ import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { constructor() { super(); } async onModuleInit() { await this.$connect(); } } ================================================ FILE: packages/bytebot-agent/src/proxy/proxy.module.ts ================================================ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ProxyService } from './proxy.service'; @Module({ imports: [ConfigModule], providers: [ProxyService], exports: [ProxyService], }) export class ProxyModule {} ================================================ FILE: packages/bytebot-agent/src/proxy/proxy.service.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import OpenAI, { APIUserAbortError } from 'openai'; import { ChatCompletionMessageParam, ChatCompletionContentPart, } from 'openai/resources/chat/completions'; import { MessageContentBlock, MessageContentType, TextContentBlock, ToolUseContentBlock, ToolResultContentBlock, ImageContentBlock, isUserActionContentBlock, isComputerToolUseContentBlock, isImageContentBlock, ThinkingContentBlock, } from '@bytebot/shared'; import { Message, Role } from '@prisma/client'; import { proxyTools } from './proxy.tools'; import { BytebotAgentService, BytebotAgentInterrupt, BytebotAgentResponse, } from '../agent/agent.types'; @Injectable() export class ProxyService implements BytebotAgentService { private readonly openai: OpenAI; private readonly logger = new Logger(ProxyService.name); constructor(private readonly configService: ConfigService) { const proxyUrl = this.configService.get('BYTEBOT_LLM_PROXY_URL'); if (!proxyUrl) { this.logger.warn( 'BYTEBOT_LLM_PROXY_URL is not set. ProxyService will not work properly.', ); } // Initialize OpenAI client with proxy configuration this.openai = new OpenAI({ apiKey: 'dummy-key-for-proxy', baseURL: proxyUrl, }); } /** * Main method to generate messages using the Chat Completions API */ async generateMessage( systemPrompt: string, messages: Message[], model: string, useTools: boolean = true, signal?: AbortSignal, ): Promise { // Convert messages to Chat Completion format const chatMessages = this.formatMessagesForChatCompletion( systemPrompt, messages, ); try { // Prepare the Chat Completion request const completionRequest: OpenAI.Chat.ChatCompletionCreateParams = { model, messages: chatMessages, max_tokens: 8192, ...(useTools && { tools: proxyTools }), reasoning_effort: 'high', }; // Make the API call const completion = await this.openai.chat.completions.create( completionRequest, { signal }, ); // Process the response const choice = completion.choices[0]; if (!choice || !choice.message) { throw new Error('No valid response from Chat Completion API'); } // Convert response to MessageContentBlocks const contentBlocks = this.formatChatCompletionResponse(choice.message); return { contentBlocks, tokenUsage: { inputTokens: completion.usage?.prompt_tokens || 0, outputTokens: completion.usage?.completion_tokens || 0, totalTokens: completion.usage?.total_tokens || 0, }, }; } catch (error: any) { if (error instanceof APIUserAbortError) { this.logger.log('Chat Completion API call aborted'); throw new BytebotAgentInterrupt(); } this.logger.error( `Error sending message to proxy: ${error.message}`, error.stack, ); throw error; } } /** * Convert Bytebot messages to Chat Completion format */ private formatMessagesForChatCompletion( systemPrompt: string, messages: Message[], ): ChatCompletionMessageParam[] { const chatMessages: ChatCompletionMessageParam[] = []; // Add system message chatMessages.push({ role: 'system', content: systemPrompt, }); // Process each message for (const message of messages) { const messageContentBlocks = message.content as MessageContentBlock[]; // Handle user actions specially if ( messageContentBlocks.every((block) => isUserActionContentBlock(block)) ) { const userActionBlocks = messageContentBlocks.flatMap( (block) => block.content, ); for (const block of userActionBlocks) { if (isComputerToolUseContentBlock(block)) { chatMessages.push({ role: 'user', content: `User performed action: ${block.name}\n${JSON.stringify( block.input, null, 2, )}`, }); } else if (isImageContentBlock(block)) { chatMessages.push({ role: 'user', content: [ { type: 'image_url', image_url: { url: `data:${block.source.media_type};base64,${block.source.data}`, detail: 'high', }, }, ], }); } } } else { for (const block of messageContentBlocks) { switch (block.type) { case MessageContentType.Text: { chatMessages.push({ role: message.role === Role.USER ? 'user' : 'assistant', content: block.text, }); break; } case MessageContentType.Image: { const imageBlock = block as ImageContentBlock; chatMessages.push({ role: 'user', content: [ { type: 'image_url', image_url: { url: `data:${imageBlock.source.media_type};base64,${imageBlock.source.data}`, detail: 'high', }, }, ], }); break; } case MessageContentType.ToolUse: { const toolBlock = block as ToolUseContentBlock; chatMessages.push({ role: 'assistant', tool_calls: [ { id: toolBlock.id, type: 'function', function: { name: toolBlock.name, arguments: JSON.stringify(toolBlock.input), }, }, ], }); break; } case MessageContentType.Thinking: { const thinkingBlock = block as ThinkingContentBlock; const message: ChatCompletionMessageParam = { role: 'assistant', content: null, }; message['reasoning_content'] = thinkingBlock.thinking; chatMessages.push(message); break; } case MessageContentType.ToolResult: { const toolResultBlock = block as ToolResultContentBlock; if ( toolResultBlock.content.every( (content) => content.type === MessageContentType.Image, ) ) { chatMessages.push({ role: 'tool', tool_call_id: toolResultBlock.tool_use_id, content: 'screenshot', }); } toolResultBlock.content.forEach((content) => { if (content.type === MessageContentType.Text) { chatMessages.push({ role: 'tool', tool_call_id: toolResultBlock.tool_use_id, content: content.text, }); } if (content.type === MessageContentType.Image) { chatMessages.push({ role: 'user', content: [ { type: 'text', text: 'Screenshot', }, { type: 'image_url', image_url: { url: `data:${content.source.media_type};base64,${content.source.data}`, detail: 'high', }, }, ], }); } }); break; } } } } } return chatMessages; } /** * Convert Chat Completion response to MessageContentBlocks */ private formatChatCompletionResponse( message: OpenAI.Chat.ChatCompletionMessage, ): MessageContentBlock[] { const contentBlocks: MessageContentBlock[] = []; // Handle text content if (message.content) { contentBlocks.push({ type: MessageContentType.Text, text: message.content, } as TextContentBlock); } if (message['reasoning_content']) { contentBlocks.push({ type: MessageContentType.Thinking, thinking: message['reasoning_content'], signature: message['reasoning_content'], } as ThinkingContentBlock); } // Handle tool calls if (message.tool_calls && message.tool_calls.length > 0) { for (const toolCall of message.tool_calls) { if (toolCall.type === 'function') { let parsedInput = {}; try { parsedInput = JSON.parse(toolCall.function.arguments || '{}'); } catch (e) { this.logger.warn( `Failed to parse tool call arguments: ${toolCall.function.arguments}`, ); parsedInput = {}; } contentBlocks.push({ type: MessageContentType.ToolUse, id: toolCall.id, name: toolCall.function.name, input: parsedInput, } as ToolUseContentBlock); } } } // Handle refusal if (message.refusal) { contentBlocks.push({ type: MessageContentType.Text, text: `Refusal: ${message.refusal}`, } as TextContentBlock); } return contentBlocks; } } ================================================ FILE: packages/bytebot-agent/src/proxy/proxy.tools.ts ================================================ import { ChatCompletionTool } from 'openai/resources'; import { agentTools } from '../agent/agent.tools'; /** * Converts an agent tool definition to OpenAI Chat Completion tool format */ function agentToolToChatCompletionTool(agentTool: any): ChatCompletionTool { return { type: 'function', function: { name: agentTool.name, description: agentTool.description, parameters: agentTool.input_schema, }, }; } /** * Convert tool name from snake_case to camelCase */ function convertToCamelCase(name: string): string { return name .split('_') .map((part, index) => { if (index === 0) return part; if (part === 'computer') return ''; return part.charAt(0).toUpperCase() + part.slice(1); }) .join('') .replace(/^computer/, ''); } /** * All tools converted to Chat Completion format */ export const proxyTools: ChatCompletionTool[] = agentTools.map((tool) => agentToolToChatCompletionTool(tool), ); /** * Individual tool exports for selective usage */ const toolMap = agentTools.reduce( (acc, tool) => { const chatCompletionTool = agentToolToChatCompletionTool(tool); const camelCaseName = convertToCamelCase(tool.name); acc[camelCaseName + 'Tool'] = chatCompletionTool; return acc; }, {} as Record, ); // Export individual tools with proper names export const moveMouseTool = toolMap.moveMouseTool; export const traceMouseTool = toolMap.traceMouseTool; export const clickMouseTool = toolMap.clickMouseTool; export const pressMouseTool = toolMap.pressMouseTool; export const dragMouseTool = toolMap.dragMouseTool; export const scrollTool = toolMap.scrollTool; export const typeKeysTool = toolMap.typeKeysTool; export const pressKeysTool = toolMap.pressKeysTool; export const typeTextTool = toolMap.typeTextTool; export const pasteTextTool = toolMap.pasteTextTool; export const waitTool = toolMap.waitTool; export const screenshotTool = toolMap.screenshotTool; export const cursorPositionTool = toolMap.cursorPositionTool; export const setTaskStatusTool = toolMap.setTaskStatusTool; export const createTaskTool = toolMap.createTaskTool; export const applicationTool = toolMap.applicationTool; ================================================ FILE: packages/bytebot-agent/src/summaries/summaries.modue.ts ================================================ import { Module } from '@nestjs/common'; import { PrismaModule } from '../prisma/prisma.module'; import { SummariesService } from './summaries.service'; @Module({ imports: [PrismaModule], providers: [SummariesService], exports: [SummariesService], }) export class SummariesModule {} ================================================ FILE: packages/bytebot-agent/src/summaries/summaries.service.ts ================================================ import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { Summary } from '@prisma/client'; @Injectable() export class SummariesService { constructor(private prisma: PrismaService) {} async create(data: { taskId: string; content: string; parentId?: string; }): Promise { return this.prisma.summary.create({ data: { taskId: data.taskId, content: data.content, ...(data.parentId ? { parentId: data.parentId } : {}), }, }); } async findLatest(taskId: string): Promise { return this.prisma.summary.findFirst({ where: { taskId }, orderBy: { createdAt: 'desc' }, }); } async findAll(taskId: string): Promise { return this.prisma.summary.findMany({ where: { taskId }, orderBy: { createdAt: 'asc' }, }); } } ================================================ FILE: packages/bytebot-agent/src/tasks/dto/add-task-message.dto.ts ================================================ import { IsNotEmpty, IsString } from 'class-validator'; export class AddTaskMessageDto { @IsNotEmpty() @IsString() message: string; } ================================================ FILE: packages/bytebot-agent/src/tasks/dto/create-task.dto.ts ================================================ import { IsArray, IsDate, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; import { Role, TaskPriority, TaskType } from '@prisma/client'; export class TaskFileDto { @IsNotEmpty() @IsString() name: string; @IsNotEmpty() @IsString() base64: string; @IsNotEmpty() @IsString() type: string; @IsNotEmpty() @IsNumber() size: number; } export class CreateTaskDto { @IsNotEmpty() @IsString() description: string; @IsOptional() @IsString() type?: TaskType; @IsOptional() @IsDate() scheduledFor?: Date; @IsOptional() @IsString() priority?: TaskPriority; @IsOptional() @IsString() createdBy?: Role; @IsOptional() model?: any; @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => TaskFileDto) files?: TaskFileDto[]; } ================================================ FILE: packages/bytebot-agent/src/tasks/dto/update-task.dto.ts ================================================ import { IsEnum, IsOptional } from 'class-validator'; import { TaskPriority, TaskStatus } from '@prisma/client'; export class UpdateTaskDto { @IsOptional() @IsEnum(TaskStatus) status?: TaskStatus; @IsOptional() @IsEnum(TaskPriority) priority?: TaskPriority; @IsOptional() queuedAt?: Date; @IsOptional() executedAt?: Date; @IsOptional() completedAt?: Date; } ================================================ FILE: packages/bytebot-agent/src/tasks/tasks.controller.ts ================================================ import { Controller, Get, Post, Body, Param, Delete, HttpStatus, HttpCode, Query, HttpException, } from '@nestjs/common'; import { TasksService } from './tasks.service'; import { CreateTaskDto } from './dto/create-task.dto'; import { Message, Task } from '@prisma/client'; import { AddTaskMessageDto } from './dto/add-task-message.dto'; import { MessagesService } from '../messages/messages.service'; import { ANTHROPIC_MODELS } from '../anthropic/anthropic.constants'; import { OPENAI_MODELS } from '../openai/openai.constants'; import { GOOGLE_MODELS } from '../google/google.constants'; import { BytebotAgentModel } from 'src/agent/agent.types'; const geminiApiKey = process.env.GEMINI_API_KEY; const anthropicApiKey = process.env.ANTHROPIC_API_KEY; const openaiApiKey = process.env.OPENAI_API_KEY; const proxyUrl = process.env.BYTEBOT_LLM_PROXY_URL; const models = [ ...(anthropicApiKey ? ANTHROPIC_MODELS : []), ...(openaiApiKey ? OPENAI_MODELS : []), ...(geminiApiKey ? GOOGLE_MODELS : []), ]; @Controller('tasks') export class TasksController { constructor( private readonly tasksService: TasksService, private readonly messagesService: MessagesService, ) {} @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createTaskDto: CreateTaskDto): Promise { return this.tasksService.create(createTaskDto); } @Get() async findAll( @Query('page') page?: string, @Query('limit') limit?: string, @Query('status') status?: string, @Query('statuses') statuses?: string, ): Promise<{ tasks: Task[]; total: number; totalPages: number }> { const pageNum = page ? parseInt(page, 10) : 1; const limitNum = limit ? parseInt(limit, 10) : 10; // Handle both single status and multiple statuses let statusFilter: string[] | undefined; if (statuses) { statusFilter = statuses.split(','); } else if (status) { statusFilter = [status]; } return this.tasksService.findAll(pageNum, limitNum, statusFilter); } @Get('models') async getModels() { if (proxyUrl) { try { const response = await fetch(`${proxyUrl}/model/info`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new HttpException( `Failed to fetch models from proxy: ${response.statusText}`, HttpStatus.BAD_GATEWAY, ); } const proxyModels = await response.json(); // Map proxy response to BytebotAgentModel format const models: BytebotAgentModel[] = proxyModels.data.map( (model: any) => ({ provider: 'proxy', name: model.litellm_params.model, title: model.model_name, contextWindow: 128000, }), ); return models; } catch (error) { if (error instanceof HttpException) { throw error; } throw new HttpException( `Error fetching models: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } return models; } @Get(':id') async findById(@Param('id') id: string): Promise { return this.tasksService.findById(id); } @Get(':id/messages') async taskMessages( @Param('id') taskId: string, @Query('limit') limit?: string, @Query('page') page?: string, ): Promise { const options = { limit: limit ? parseInt(limit, 10) : undefined, page: page ? parseInt(page, 10) : undefined, }; const messages = await this.messagesService.findAll(taskId, options); return messages; } @Post(':id/messages') @HttpCode(HttpStatus.CREATED) async addTaskMessage( @Param('id') taskId: string, @Body() guideTaskDto: AddTaskMessageDto, ): Promise { return this.tasksService.addTaskMessage(taskId, guideTaskDto); } @Get(':id/messages/raw') async taskRawMessages( @Param('id') taskId: string, @Query('limit') limit?: string, @Query('page') page?: string, ): Promise { const options = { limit: limit ? parseInt(limit, 10) : undefined, page: page ? parseInt(page, 10) : undefined, }; return this.messagesService.findRawMessages(taskId, options); } @Get(':id/messages/processed') async taskProcessedMessages( @Param('id') taskId: string, @Query('limit') limit?: string, @Query('page') page?: string, ) { const options = { limit: limit ? parseInt(limit, 10) : undefined, page: page ? parseInt(page, 10) : undefined, }; return this.messagesService.findProcessedMessages(taskId, options); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async delete(@Param('id') id: string): Promise { await this.tasksService.delete(id); } @Post(':id/takeover') @HttpCode(HttpStatus.OK) async takeOver(@Param('id') taskId: string): Promise { return this.tasksService.takeOver(taskId); } @Post(':id/resume') @HttpCode(HttpStatus.OK) async resume(@Param('id') taskId: string): Promise { return this.tasksService.resume(taskId); } @Post(':id/cancel') @HttpCode(HttpStatus.OK) async cancel(@Param('id') taskId: string): Promise { return this.tasksService.cancel(taskId); } } ================================================ FILE: packages/bytebot-agent/src/tasks/tasks.gateway.ts ================================================ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Injectable } from '@nestjs/common'; @Injectable() @WebSocketGateway({ cors: { origin: '*', methods: ['GET', 'POST'], }, }) export class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('join_task') handleJoinTask(client: Socket, taskId: string) { client.join(`task_${taskId}`); console.log(`Client ${client.id} joined task ${taskId}`); } @SubscribeMessage('leave_task') handleLeaveTask(client: Socket, taskId: string) { client.leave(`task_${taskId}`); console.log(`Client ${client.id} left task ${taskId}`); } emitTaskUpdate(taskId: string, task: any) { this.server.to(`task_${taskId}`).emit('task_updated', task); } emitNewMessage(taskId: string, message: any) { this.server.to(`task_${taskId}`).emit('new_message', message); } emitTaskCreated(task: any) { this.server.emit('task_created', task); } emitTaskDeleted(taskId: string) { this.server.emit('task_deleted', taskId); } } ================================================ FILE: packages/bytebot-agent/src/tasks/tasks.module.ts ================================================ import { Module } from '@nestjs/common'; import { TasksController } from './tasks.controller'; import { TasksService } from './tasks.service'; import { TasksGateway } from './tasks.gateway'; import { PrismaModule } from '../prisma/prisma.module'; import { MessagesModule } from '../messages/messages.module'; @Module({ imports: [PrismaModule, MessagesModule], controllers: [TasksController], providers: [TasksService, TasksGateway], exports: [TasksService, TasksGateway], }) export class TasksModule {} ================================================ FILE: packages/bytebot-agent/src/tasks/tasks.service.ts ================================================ import { Injectable, NotFoundException, Logger, BadRequestException, Inject, forwardRef, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateTaskDto } from './dto/create-task.dto'; import { UpdateTaskDto } from './dto/update-task.dto'; import { Task, Role, Prisma, TaskStatus, TaskType, TaskPriority, File, } from '@prisma/client'; import { AddTaskMessageDto } from './dto/add-task-message.dto'; import { TasksGateway } from './tasks.gateway'; import { ConfigService } from '@nestjs/config'; import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class TasksService { private readonly logger = new Logger(TasksService.name); constructor( readonly prisma: PrismaService, @Inject(forwardRef(() => TasksGateway)) private readonly tasksGateway: TasksGateway, private readonly configService: ConfigService, private readonly eventEmitter: EventEmitter2, ) { this.logger.log('TasksService initialized'); } async create(createTaskDto: CreateTaskDto): Promise { this.logger.log( `Creating new task with description: ${createTaskDto.description}`, ); const task = await this.prisma.$transaction(async (prisma) => { // Create the task first this.logger.debug('Creating task record in database'); const task = await prisma.task.create({ data: { description: createTaskDto.description, type: createTaskDto.type || TaskType.IMMEDIATE, priority: createTaskDto.priority || TaskPriority.MEDIUM, status: TaskStatus.PENDING, createdBy: createTaskDto.createdBy || Role.USER, model: createTaskDto.model, ...(createTaskDto.scheduledFor ? { scheduledFor: createTaskDto.scheduledFor } : {}), }, }); this.logger.log(`Task created successfully with ID: ${task.id}`); let filesDescription = ''; // Save files if provided if (createTaskDto.files && createTaskDto.files.length > 0) { this.logger.debug( `Saving ${createTaskDto.files.length} file(s) for task ID: ${task.id}`, ); filesDescription += `\n`; const filePromises = createTaskDto.files.map((file) => { // Extract base64 data without the data URL prefix const base64Data = file.base64.includes('base64,') ? file.base64.split('base64,')[1] : file.base64; filesDescription += `\nFile ${file.name} written to desktop.`; return prisma.file.create({ data: { name: file.name, type: file.type || 'application/octet-stream', size: file.size, data: base64Data, taskId: task.id, }, }); }); await Promise.all(filePromises); this.logger.debug(`Files saved successfully for task ID: ${task.id}`); } // Create the initial system message this.logger.debug(`Creating initial message for task ID: ${task.id}`); await prisma.message.create({ data: { content: [ { type: 'text', text: `${createTaskDto.description} ${filesDescription}`, }, ] as Prisma.InputJsonValue, role: Role.USER, taskId: task.id, }, }); this.logger.debug(`Initial message created for task ID: ${task.id}`); return task; }); this.tasksGateway.emitTaskCreated(task); return task; } async findScheduledTasks(): Promise { return this.prisma.task.findMany({ where: { scheduledFor: { not: null, }, queuedAt: null, }, orderBy: [{ scheduledFor: 'asc' }], }); } async findNextTask(): Promise<(Task & { files: File[] }) | null> { const task = await this.prisma.task.findFirst({ where: { status: { in: [TaskStatus.RUNNING, TaskStatus.PENDING], }, }, orderBy: [ { executedAt: 'asc' }, { priority: 'desc' }, { queuedAt: 'asc' }, { createdAt: 'asc' }, ], include: { files: true, }, }); if (task) { this.logger.log( `Found existing task with ID: ${task.id}, and status ${task.status}. Resuming.`, ); } return task; } async findAll( page = 1, limit = 10, statuses?: string[], ): Promise<{ tasks: Task[]; total: number; totalPages: number }> { this.logger.log( `Retrieving tasks - page: ${page}, limit: ${limit}, statuses: ${statuses?.join(',')}`, ); const skip = (page - 1) * limit; const whereClause: Prisma.TaskWhereInput = statuses && statuses.length > 0 ? { status: { in: statuses as TaskStatus[] } } : {}; const [tasks, total] = await Promise.all([ this.prisma.task.findMany({ where: whereClause, orderBy: { createdAt: 'desc', }, skip, take: limit, }), this.prisma.task.count({ where: whereClause }), ]); const totalPages = Math.ceil(total / limit); this.logger.debug(`Retrieved ${tasks.length} tasks out of ${total} total`); return { tasks, total, totalPages }; } async findById(id: string): Promise { this.logger.log(`Retrieving task by ID: ${id}`); try { const task = await this.prisma.task.findUnique({ where: { id }, include: { files: true, }, }); if (!task) { this.logger.warn(`Task with ID: ${id} not found`); throw new NotFoundException(`Task with ID ${id} not found`); } this.logger.debug(`Retrieved task with ID: ${id}`); return task; } catch (error: any) { this.logger.error(`Error retrieving task ID: ${id} - ${error.message}`); this.logger.error(error.stack); throw error; } } async update(id: string, updateTaskDto: UpdateTaskDto): Promise { this.logger.log(`Updating task with ID: ${id}`); this.logger.debug(`Update data: ${JSON.stringify(updateTaskDto)}`); const existingTask = await this.findById(id); if (!existingTask) { this.logger.warn(`Task with ID: ${id} not found for update`); throw new NotFoundException(`Task with ID ${id} not found`); } let updatedTask = await this.prisma.task.update({ where: { id }, data: updateTaskDto, }); if (updateTaskDto.status === TaskStatus.COMPLETED) { this.eventEmitter.emit('task.completed', { taskId: id }); } else if (updateTaskDto.status === TaskStatus.NEEDS_HELP) { updatedTask = await this.takeOver(id); } else if (updateTaskDto.status === TaskStatus.FAILED) { this.eventEmitter.emit('task.failed', { taskId: id }); } this.logger.log(`Successfully updated task ID: ${id}`); this.logger.debug(`Updated task: ${JSON.stringify(updatedTask)}`); this.tasksGateway.emitTaskUpdate(id, updatedTask); return updatedTask; } async delete(id: string): Promise { this.logger.log(`Deleting task with ID: ${id}`); const deletedTask = await this.prisma.task.delete({ where: { id }, }); this.logger.log(`Successfully deleted task ID: ${id}`); this.tasksGateway.emitTaskDeleted(id); return deletedTask; } async addTaskMessage(taskId: string, addTaskMessageDto: AddTaskMessageDto) { const task = await this.findById(taskId); if (!task) { this.logger.warn(`Task with ID: ${taskId} not found for guiding`); throw new NotFoundException(`Task with ID ${taskId} not found`); } const message = await this.prisma.message.create({ data: { content: [{ type: 'text', text: addTaskMessageDto.message }], role: Role.USER, taskId, }, }); this.tasksGateway.emitNewMessage(taskId, message); return task; } async resume(taskId: string): Promise { this.logger.log(`Resuming task ID: ${taskId}`); const task = await this.findById(taskId); if (!task) { throw new NotFoundException(`Task with ID ${taskId} not found`); } if (task.control !== Role.USER) { throw new BadRequestException(`Task ${taskId} is not under user control`); } const updatedTask = await this.prisma.task.update({ where: { id: taskId }, data: { control: Role.ASSISTANT, status: TaskStatus.RUNNING, }, }); try { await fetch( `${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/stop`, { method: 'POST' }, ); } catch (error) { this.logger.error('Failed to stop input tracking', error); } // Broadcast resume event so AgentProcessor can react this.eventEmitter.emit('task.resume', { taskId }); this.logger.log(`Task ${taskId} resumed`); this.tasksGateway.emitTaskUpdate(taskId, updatedTask); return updatedTask; } async takeOver(taskId: string): Promise { this.logger.log(`Taking over control for task ID: ${taskId}`); const task = await this.findById(taskId); if (!task) { throw new NotFoundException(`Task with ID ${taskId} not found`); } if (task.control !== Role.ASSISTANT) { throw new BadRequestException( `Task ${taskId} is not under agent control`, ); } const updatedTask = await this.prisma.task.update({ where: { id: taskId }, data: { control: Role.USER, }, }); try { await fetch( `${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/start`, { method: 'POST' }, ); } catch (error) { this.logger.error('Failed to start input tracking', error); } // Broadcast takeover event so AgentProcessor can react this.eventEmitter.emit('task.takeover', { taskId }); this.logger.log(`Task ${taskId} takeover initiated`); this.tasksGateway.emitTaskUpdate(taskId, updatedTask); return updatedTask; } async cancel(taskId: string): Promise { this.logger.log(`Cancelling task ID: ${taskId}`); const task = await this.findById(taskId); if (!task) { throw new NotFoundException(`Task with ID ${taskId} not found`); } if ( task.status === TaskStatus.COMPLETED || task.status === TaskStatus.FAILED || task.status === TaskStatus.CANCELLED ) { throw new BadRequestException( `Task ${taskId} is already completed, failed, or cancelled`, ); } const updatedTask = await this.prisma.task.update({ where: { id: taskId }, data: { status: TaskStatus.CANCELLED, }, }); // Broadcast cancel event so AgentProcessor can cancel processing this.eventEmitter.emit('task.cancel', { taskId }); this.logger.log(`Task ${taskId} cancelled and marked as failed`); this.tasksGateway.emitTaskUpdate(taskId, updatedTask); return updatedTask; } } ================================================ FILE: packages/bytebot-agent/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] } ================================================ FILE: packages/bytebot-agent/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "incremental": true, "skipLibCheck": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, "noFallthroughCasesInSwitch": false } } ================================================ FILE: packages/bytebot-agent-cc/.dockerignore ================================================ **/node_modules **/dist **/.git **/.vscode **/.env* **/npm-debug.log **/yarn-debug.log **/yarn-error.log **/package-lock.json ================================================ FILE: packages/bytebot-agent-cc/.gitignore ================================================ # compiled output /dist /node_modules /build # Logs logs *.log npm-debug.log* pnpm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # OS .DS_Store # Tests /coverage /.nyc_output # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # temp directory .temp .tmp # Runtime data pids *.pid *.seed *.pid.lock # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json ================================================ FILE: packages/bytebot-agent-cc/.prettierrc ================================================ { "singleQuote": true, "trailingComma": "all" } ================================================ FILE: packages/bytebot-agent-cc/Dockerfile ================================================ # Base image FROM node:20-alpine # Create app directory WORKDIR /app # Copy app source COPY ./shared ./shared COPY ./bytebot-agent-cc/ ./bytebot-agent-cc/ WORKDIR /app/bytebot-agent-cc # Install dependencies RUN npm install RUN npm run build # Run the application CMD ["npm", "run", "start:prod"] ================================================ FILE: packages/bytebot-agent-cc/eslint.config.mjs ================================================ // @ts-check import eslint from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import globals from 'globals'; import tseslint from 'typescript-eslint'; export default tseslint.config( { ignores: ['eslint.config.mjs'], }, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, eslintPluginPrettierRecommended, { languageOptions: { globals: { ...globals.node, ...globals.jest, }, ecmaVersion: 5, sourceType: 'module', parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname, }, }, }, { rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn' }, }, ); ================================================ FILE: packages/bytebot-agent-cc/nest-cli.json ================================================ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true } } ================================================ FILE: packages/bytebot-agent-cc/package.json ================================================ { "name": "bytebot-agent", "version": "0.0.1", "description": "", "author": "", "private": true, "license": "UNLICENSED", "scripts": { "prisma:dev": "npx prisma migrate dev && npx prisma generate", "prisma:prod": "npx prisma migrate deploy && npx prisma generate", "build": "npm run build --prefix ../shared && npx prisma generate && nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "npm run build --prefix ../shared && nest start", "start:dev": "npm run build --prefix ../shared && nest start --watch", "start:debug": "npm run build --prefix ../shared && nest start --debug --watch", "start:prod": "npm run build --prefix ../shared && npx prisma migrate deploy && npx prisma generate && node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { "@anthropic-ai/claude-code": "^1.0.105", "@anthropic-ai/sdk": "^0.39.0", "@bytebot/shared": "../shared", "@google/genai": "^1.8.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/event-emitter": "^3.0.1", "@nestjs/platform-express": "^11.1.5", "@nestjs/platform-socket.io": "^11.1.1", "@nestjs/schedule": "^6.0.0", "@nestjs/websockets": "^11.1.1", "@prisma/client": "^6.16.1", "@thallesp/nestjs-better-auth": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "openai": "^5.8.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "zod": "^4.0.5" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", "globals": "^15.14.0", "jest": "^29.7.0", "prettier": "^3.4.2", "prisma": "^6.16.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.7.3", "typescript-eslint": "^8.20.0" }, "jest": { "moduleFileExtensions": [ "js", "json", "ts" ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", "testEnvironment": "node" }, "overrides": { "openai": { "zod": "^4.0.5" } }, "engines": { "node": "20" } } ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250328022708_initial_migration/migration.sql ================================================ -- CreateEnum CREATE TYPE "TaskStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED'); -- CreateEnum CREATE TYPE "TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT'); -- CreateEnum CREATE TYPE "MessageType" AS ENUM ('USER', 'ASSISTANT'); -- CreateTable CREATE TABLE "Task" ( "id" TEXT NOT NULL, "description" TEXT NOT NULL, "status" "TaskStatus" NOT NULL DEFAULT 'PENDING', "priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Task_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Summary" ( "id" TEXT NOT NULL, "content" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "taskId" TEXT NOT NULL, "parentId" TEXT, CONSTRAINT "Summary_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Message" ( "id" TEXT NOT NULL, "content" JSONB NOT NULL, "type" "MessageType" NOT NULL DEFAULT 'ASSISTANT', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "taskId" TEXT NOT NULL, "summaryId" TEXT, CONSTRAINT "Message_pkey" PRIMARY KEY ("id") ); -- AddForeignKey ALTER TABLE "Summary" ADD CONSTRAINT "Summary_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Summary" ADD CONSTRAINT "Summary_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Summary"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_summaryId_fkey" FOREIGN KEY ("summaryId") REFERENCES "Summary"("id") ON DELETE SET NULL ON UPDATE CASCADE; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250413053912_message_role/migration.sql ================================================ /* Warnings: - You are about to drop the column `type` on the `Message` table. All the data in the column will be lost. */ -- CreateEnum CREATE TYPE "MessageRole" AS ENUM ('USER', 'ASSISTANT'); -- AlterTable ALTER TABLE "Message" DROP COLUMN "type", ADD COLUMN "role" "MessageRole" NOT NULL DEFAULT 'ASSISTANT'; -- DropEnum DROP TYPE "MessageType"; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250522200556_updated_task_structure/migration.sql ================================================ -- CreateEnum CREATE TYPE "Role" AS ENUM ('USER', 'ASSISTANT'); -- CreateEnum CREATE TYPE "TaskType" AS ENUM ('IMMEDIATE', 'SCHEDULED'); -- AlterEnum BEGIN; CREATE TYPE "TaskStatus_new" AS ENUM ('PENDING', 'RUNNING', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED'); ALTER TABLE "Task" ALTER COLUMN "status" DROP DEFAULT; ALTER TABLE "Task" ALTER COLUMN "status" TYPE "TaskStatus_new" USING (CASE "status"::text WHEN 'IN_PROGRESS' THEN 'RUNNING' ELSE "status"::text END::"TaskStatus_new"); ALTER TYPE "TaskStatus" RENAME TO "TaskStatus_old"; ALTER TYPE "TaskStatus_new" RENAME TO "TaskStatus"; DROP TYPE "TaskStatus_old"; ALTER TABLE "Task" ALTER COLUMN "status" SET DEFAULT 'PENDING'; COMMIT; -- DropForeignKey ALTER TABLE "Message" DROP CONSTRAINT "Message_taskId_fkey"; -- DropForeignKey ALTER TABLE "Summary" DROP CONSTRAINT "Summary_taskId_fkey"; -- AlterTable ALTER TABLE "Message" ADD COLUMN "new_role" "Role" NOT NULL DEFAULT 'ASSISTANT'; UPDATE "Message" SET "new_role" = CASE WHEN lower("role"::text) = 'user' THEN 'USER'::"Role" WHEN lower("role"::text) = 'assistant' THEN 'ASSISTANT'::"Role" ELSE 'ASSISTANT'::"Role" END; -- Step 3: Drop the old 'role' column. ALTER TABLE "Message" DROP COLUMN "role"; -- Step 4: Rename 'new_role' to 'role'. ALTER TABLE "Message" RENAME COLUMN "new_role" TO "role"; -- AlterTable ALTER TABLE "Task" ADD COLUMN "completedAt" TIMESTAMP(3), ADD COLUMN "createdBy" "Role" NOT NULL DEFAULT 'USER', ADD COLUMN "error" TEXT, ADD COLUMN "executedAt" TIMESTAMP(3), ADD COLUMN "result" JSONB, ADD COLUMN "type" "TaskType" NOT NULL DEFAULT 'IMMEDIATE'; -- DropEnum DROP TYPE "MessageRole"; -- AddForeignKey ALTER TABLE "Summary" ADD CONSTRAINT "Summary_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250523162632_add_scheduling/migration.sql ================================================ -- AlterTable ALTER TABLE "Task" ADD COLUMN "queuedAt" TIMESTAMP(3), ADD COLUMN "scheduledFor" TIMESTAMP(3); ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250529003255_tasks_control/migration.sql ================================================ -- AlterTable ALTER TABLE "Task" ADD COLUMN "control" "Role" NOT NULL DEFAULT 'USER'; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250530012753_tasks_control/migration.sql ================================================ -- AlterTable ALTER TABLE "Task" ALTER COLUMN "control" SET DEFAULT 'ASSISTANT'; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250619013027_add_better_auth_schema/migration.sql ================================================ -- AlterTable ALTER TABLE "Message" ADD COLUMN "userId" TEXT; -- CreateTable CREATE TABLE "User" ( "id" TEXT NOT NULL, "name" TEXT, "email" TEXT NOT NULL, "emailVerified" BOOLEAN NOT NULL DEFAULT false, "image" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "User_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Session" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "token" TEXT NOT NULL, "expiresAt" TIMESTAMP(3) NOT NULL, "ipAddress" TEXT, "userAgent" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Session_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Account" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "accountId" TEXT NOT NULL, "providerId" TEXT NOT NULL, "accessToken" TEXT, "refreshToken" TEXT, "accessTokenExpiresAt" TIMESTAMP(3), "refreshTokenExpiresAt" TIMESTAMP(3), "scope" TEXT, "idToken" TEXT, "password" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Account_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Verification" ( "id" TEXT NOT NULL, "identifier" TEXT NOT NULL, "value" TEXT NOT NULL, "expiresAt" TIMESTAMP(3) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Verification_pkey" PRIMARY KEY ("id") ); -- CreateIndex CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -- CreateIndex CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token"); -- CreateIndex CREATE UNIQUE INDEX "Account_providerId_accountId_key" ON "Account"("providerId", "accountId"); -- CreateIndex CREATE UNIQUE INDEX "Verification_identifier_value_key" ON "Verification"("identifier", "value"); -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250622195148_add_user_to_task/migration.sql ================================================ -- AlterTable ALTER TABLE "Task" ADD COLUMN "userId" TEXT; -- AddForeignKey ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250706223912_model_picker/migration.sql ================================================ -- AlterTable: add `model` column as JSONB (nullable initially) ALTER TABLE "Task" ADD COLUMN "model" JSONB; -- Backfill existing tasks with the default Anthropic Claude Opus 4 model UPDATE "Task" SET "model" = jsonb_build_object( 'provider', 'anthropic', 'name', 'claude-opus-4-20250514', 'title', 'Claude Opus 4' ) WHERE "model" IS NULL; -- Enforce NOT NULL constraint now that data is populated ALTER TABLE "Task" ALTER COLUMN "model" SET NOT NULL; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250722041608_files/migration.sql ================================================ -- CreateTable CREATE TABLE "File" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "type" TEXT NOT NULL, "size" INTEGER NOT NULL, "data" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "taskId" TEXT NOT NULL, CONSTRAINT "File_pkey" PRIMARY KEY ("id") ); -- AddForeignKey ALTER TABLE "File" ADD CONSTRAINT "File_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/20250820172813_remove_auth/migration.sql ================================================ /* Warnings: - You are about to drop the column `userId` on the `Message` table. All the data in the column will be lost. - You are about to drop the column `userId` on the `Task` table. All the data in the column will be lost. - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost. - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost. - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. - You are about to drop the `Verification` table. If the table is not empty, all the data it contains will be lost. */ -- DropForeignKey ALTER TABLE "public"."Account" DROP CONSTRAINT "Account_userId_fkey"; -- DropForeignKey ALTER TABLE "public"."Message" DROP CONSTRAINT "Message_userId_fkey"; -- DropForeignKey ALTER TABLE "public"."Session" DROP CONSTRAINT "Session_userId_fkey"; -- DropForeignKey ALTER TABLE "public"."Task" DROP CONSTRAINT "Task_userId_fkey"; -- AlterTable ALTER TABLE "public"."Message" DROP COLUMN "userId"; -- AlterTable ALTER TABLE "public"."Task" DROP COLUMN "userId"; -- DropTable DROP TABLE "public"."Account"; -- DropTable DROP TABLE "public"."Session"; -- DropTable DROP TABLE "public"."User"; -- DropTable DROP TABLE "public"."Verification"; ================================================ FILE: packages/bytebot-agent-cc/prisma/migrations/migration_lock.toml ================================================ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) provider = "postgresql" ================================================ FILE: packages/bytebot-agent-cc/prisma/schema.prisma ================================================ // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum TaskStatus { PENDING RUNNING NEEDS_HELP NEEDS_REVIEW COMPLETED CANCELLED FAILED } enum TaskPriority { LOW MEDIUM HIGH URGENT } enum Role { USER ASSISTANT } enum TaskType { IMMEDIATE SCHEDULED } model Task { id String @id @default(uuid()) description String type TaskType @default(IMMEDIATE) status TaskStatus @default(PENDING) priority TaskPriority @default(MEDIUM) control Role @default(ASSISTANT) createdAt DateTime @default(now()) createdBy Role @default(USER) scheduledFor DateTime? updatedAt DateTime @updatedAt executedAt DateTime? completedAt DateTime? queuedAt DateTime? error String? result Json? // Example: // { "provider": "anthropic", "name": "claude-opus-4-20250514", "title": "Claude Opus 4" } model Json messages Message[] summaries Summary[] files File[] } model Summary { id String @id @default(uuid()) content String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt messages Message[] // One-to-many relationship: one Summary has many Messages task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) taskId String // Self-referential relationship parentSummary Summary? @relation("SummaryHierarchy", fields: [parentId], references: [id]) parentId String? childSummaries Summary[] @relation("SummaryHierarchy") } model Message { id String @id @default(uuid()) // Content field follows Anthropic's content blocks structure // Example: // [ // {"type": "text", "text": "Hello world"}, // {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "..."}} // ] content Json role Role @default(ASSISTANT) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) taskId String summary Summary? @relation(fields: [summaryId], references: [id]) summaryId String? // Optional foreign key to Summary } model File { id String @id @default(uuid()) name String type String // MIME type size Int // Size in bytes data String // Base64 encoded file data createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) taskId String } ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.analytics.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { ConfigService } from '@nestjs/config'; import { TasksService } from '../tasks/tasks.service'; import { MessagesService } from '../messages/messages.service'; @Injectable() export class AgentAnalyticsService { private readonly logger = new Logger(AgentAnalyticsService.name); private readonly endpoint?: string; constructor( private readonly tasksService: TasksService, private readonly messagesService: MessagesService, configService: ConfigService, ) { this.endpoint = configService.get('BYTEBOT_ANALYTICS_ENDPOINT'); if (!this.endpoint) { this.logger.warn( 'BYTEBOT_ANALYTICS_ENDPOINT is not set. Analytics service disabled.', ); } } @OnEvent('task.cancel') @OnEvent('task.failed') @OnEvent('task.completed') async handleTaskEvent(payload: { taskId: string }) { if (!this.endpoint) return; try { const task = await this.tasksService.findById(payload.taskId); const messages = await this.messagesService.findEvery(payload.taskId); await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...task, messages }), }); } catch (error: any) { this.logger.error( `Failed to send analytics for task ${payload.taskId}: ${error.message}`, error.stack, ); } } } ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.computer-use.ts ================================================ import { Button, Coordinates, Press, ComputerToolUseContentBlock, ToolResultContentBlock, MessageContentType, isScreenshotToolUseBlock, isCursorPositionToolUseBlock, isMoveMouseToolUseBlock, isTraceMouseToolUseBlock, isClickMouseToolUseBlock, isPressMouseToolUseBlock, isDragMouseToolUseBlock, isScrollToolUseBlock, isTypeKeysToolUseBlock, isPressKeysToolUseBlock, isTypeTextToolUseBlock, isWaitToolUseBlock, isApplicationToolUseBlock, isPasteTextToolUseBlock, isReadFileToolUseBlock, } from '@bytebot/shared'; import { Logger } from '@nestjs/common'; const BYTEBOT_DESKTOP_BASE_URL = process.env.BYTEBOT_DESKTOP_BASE_URL as string; export async function handleComputerToolUse( block: ComputerToolUseContentBlock, logger: Logger, ): Promise { logger.debug( `Handling computer tool use: ${block.name}, tool_use_id: ${block.id}`, ); if (isScreenshotToolUseBlock(block)) { logger.debug('Processing screenshot request'); try { logger.debug('Taking screenshot'); const image = await screenshot(); logger.debug('Screenshot captured successfully'); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Image, source: { data: image, media_type: 'image/png', type: 'base64', }, }, ], }; } catch (error) { logger.error(`Screenshot failed: ${error.message}`, error.stack); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'ERROR: Failed to take screenshot', }, ], is_error: true, }; } } if (isCursorPositionToolUseBlock(block)) { logger.debug('Processing cursor position request'); try { logger.debug('Getting cursor position'); const position = await cursorPosition(); logger.debug(`Cursor position obtained: ${position.x}, ${position.y}`); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: `Cursor position: ${position.x}, ${position.y}`, }, ], }; } catch (error) { logger.error( `Getting cursor position failed: ${error.message}`, error.stack, ); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'ERROR: Failed to get cursor position', }, ], is_error: true, }; } } try { if (isMoveMouseToolUseBlock(block)) { await moveMouse(block.input); } if (isTraceMouseToolUseBlock(block)) { await traceMouse(block.input); } if (isClickMouseToolUseBlock(block)) { await clickMouse(block.input); } if (isPressMouseToolUseBlock(block)) { await pressMouse(block.input); } if (isDragMouseToolUseBlock(block)) { await dragMouse(block.input); } if (isScrollToolUseBlock(block)) { await scroll(block.input); } if (isTypeKeysToolUseBlock(block)) { await typeKeys(block.input); } if (isPressKeysToolUseBlock(block)) { await pressKeys(block.input); } if (isTypeTextToolUseBlock(block)) { await typeText(block.input); } if (isPasteTextToolUseBlock(block)) { await pasteText(block.input); } if (isWaitToolUseBlock(block)) { await wait(block.input); } if (isApplicationToolUseBlock(block)) { await application(block.input); } if (isReadFileToolUseBlock(block)) { logger.debug(`Reading file: ${block.input.path}`); const result = await readFile(block.input); if (result.success && result.data) { // Return document content block return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Document, source: { type: 'base64', media_type: result.mediaType || 'application/octet-stream', data: result.data, }, name: result.name || 'file', size: result.size, }, ], }; } else { // Return error message return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: result.message || 'Error reading file', }, ], is_error: true, }; } } let image: string | null = null; try { logger.debug('Taking screenshot'); image = await screenshot(); logger.debug('Screenshot captured successfully'); } catch (error) { logger.error('Failed to take screenshot', error); } logger.debug(`Tool execution successful for tool_use_id: ${block.id}`); const toolResult: ToolResultContentBlock = { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: 'Tool executed successfully', }, ], }; if (image) { toolResult.content.push({ type: MessageContentType.Image, source: { data: image, media_type: 'image/png', type: 'base64', }, }); } return toolResult; } catch (error) { logger.error( `Error executing ${block.name} tool: ${error.message}`, error.stack, ); return { type: MessageContentType.ToolResult, tool_use_id: block.id, content: [ { type: MessageContentType.Text, text: `Error executing ${block.name} tool: ${error.message}`, }, ], is_error: true, }; } } async function moveMouse(input: { coordinates: Coordinates }): Promise { const { coordinates } = input; console.log( `Moving mouse to coordinates: [${coordinates.x}, ${coordinates.y}]`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'move_mouse', coordinates, }), }); } catch (error) { console.error('Error in move_mouse action:', error); throw error; } } async function traceMouse(input: { path: Coordinates[]; holdKeys?: string[]; }): Promise { const { path, holdKeys } = input; console.log( `Tracing mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'trace_mouse', path, holdKeys, }), }); } catch (error) { console.error('Error in trace_mouse action:', error); throw error; } } async function clickMouse(input: { coordinates?: Coordinates; button: Button; holdKeys?: string[]; clickCount: number; }): Promise { const { coordinates, button, holdKeys, clickCount } = input; console.log( `Clicking mouse ${button} ${clickCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}] ` : ''} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'click_mouse', coordinates, button, holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined, clickCount, }), }); } catch (error) { console.error('Error in click_mouse action:', error); throw error; } } async function pressMouse(input: { coordinates?: Coordinates; button: Button; press: Press; }): Promise { const { coordinates, button, press } = input; console.log( `Pressing mouse ${button} ${press} ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'press_mouse', coordinates, button, press, }), }); } catch (error) { console.error('Error in press_mouse action:', error); throw error; } } async function dragMouse(input: { path: Coordinates[]; button: Button; holdKeys?: string[]; }): Promise { const { path, button, holdKeys } = input; console.log( `Dragging mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'drag_mouse', path, button, holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined, }), }); } catch (error) { console.error('Error in drag_mouse action:', error); throw error; } } async function scroll(input: { coordinates?: Coordinates; direction: 'up' | 'down' | 'left' | 'right'; scrollCount: number; holdKeys?: string[]; }): Promise { const { coordinates, direction, scrollCount, holdKeys } = input; console.log( `Scrolling ${direction} ${scrollCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`, ); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'scroll', coordinates, direction, scrollCount, holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined, }), }); } catch (error) { console.error('Error in scroll action:', error); throw error; } } async function typeKeys(input: { keys: string[]; delay?: number; }): Promise { const { keys, delay } = input; console.log(`Typing keys: ${keys}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'type_keys', keys, delay, }), }); } catch (error) { console.error('Error in type_keys action:', error); throw error; } } async function pressKeys(input: { keys: string[]; press: Press; }): Promise { const { keys, press } = input; console.log(`Pressing keys: ${keys}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'press_keys', keys, press, }), }); } catch (error) { console.error('Error in press_keys action:', error); throw error; } } async function typeText(input: { text: string; delay?: number; }): Promise { const { text, delay } = input; console.log(`Typing text: ${text}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'type_text', text, delay, }), }); } catch (error) { console.error('Error in type_text action:', error); throw error; } } async function pasteText(input: { text: string }): Promise { const { text } = input; console.log(`Pasting text: ${text}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'paste_text', text, }), }); } catch (error) { console.error('Error in paste_text action:', error); throw error; } } async function wait(input: { duration: number }): Promise { const { duration } = input; console.log(`Waiting for ${duration}ms`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'wait', duration, }), }); } catch (error) { console.error('Error in wait action:', error); throw error; } } async function cursorPosition(): Promise { console.log('Getting cursor position'); try { const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'cursor_position', }), }); const data = await response.json(); return { x: data.x, y: data.y }; } catch (error) { console.error('Error in cursor_position action:', error); throw error; } } async function screenshot(): Promise { console.log('Taking screenshot'); try { const requestBody = { action: 'screenshot', }; const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`Failed to take screenshot: ${response.statusText}`); } const data = await response.json(); if (!data.image) { throw new Error('Failed to take screenshot: No image data received'); } return data.image; // Base64 encoded image } catch (error) { console.error('Error in screenshot action:', error); throw error; } } async function application(input: { application: string }): Promise { const { application } = input; console.log(`Opening application: ${application}`); try { await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'application', application, }), }); } catch (error) { console.error('Error in application action:', error); throw error; } } async function readFile(input: { path: string }): Promise<{ success: boolean; data?: string; name?: string; size?: number; mediaType?: string; message?: string; }> { const { path } = input; console.log(`Reading file: ${path}`); try { const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'read_file', path, }), }); if (!response.ok) { throw new Error(`Failed to read file: ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { console.error('Error in read_file action:', error); return { success: false, message: `Error reading file: ${error.message}`, }; } } export async function writeFile(input: { path: string; content: string; }): Promise<{ success: boolean; message?: string }> { const { path, content } = input; console.log(`Writing file: ${path}`); try { // Content is always base64 encoded const base64Data = content; const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'write_file', path, data: base64Data, }), }); if (!response.ok) { throw new Error(`Failed to write file: ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { console.error('Error in write_file action:', error); return { success: false, message: `Error writing file: ${error.message}`, }; } } ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.constants.ts ================================================ export const DEFAULT_DISPLAY_SIZE = { width: 1280, height: 960, }; export const SUMMARIZATION_SYSTEM_PROMPT = `You are a helpful assistant that summarizes conversations for long-running tasks. Your job is to create concise summaries that preserve all important information, tool usage, and key decisions. Focus on: - Task progress and completed actions - Important tool calls and their results - Key decisions made - Any errors or issues encountered - Current state and what remains to be done Provide a structured summary that can be used as context for continuing the task.`; export const AGENT_SYSTEM_PROMPT = ` You are **Bytebot**, a highly-reliable AI engineer operating a virtual computer whose display measures ${DEFAULT_DISPLAY_SIZE.width} x ${DEFAULT_DISPLAY_SIZE.height} pixels. The current date is ${new Date().toLocaleDateString()}. The current time is ${new Date().toLocaleTimeString()}. The current timezone is ${Intl.DateTimeFormat().resolvedOptions().timeZone}. ──────────────────────── AVAILABLE APPLICATIONS ──────────────────────── On the desktop, the following applications are available: Firefox Browser -- The default web browser, use it to navigate to websites. Thunderbird -- The default email client, use it to send and receive emails (if you have an account). 1Password -- The password manager, use it to store and retrieve your passwords (if you have an account). Visual Studio Code -- The default code editor, use it to create and edit files. Terminal -- The default terminal, use it to run commands. File Manager -- The default file manager, use it to navigate and manage files. Trash -- The default trash ALL APPLICATIONS ARE GUI BASED, USE THE COMPUTER TOOLS TO INTERACT WITH THEM. ONLY ACCESS THE APPLICATIONS VIA THEIR DESKTOP ICONS. *Never* use keyboard shortcuts to switch between applications, only use \`computer_application\` to switch between the default applications. ──────────────────────── CORE WORKING PRINCIPLES ──────────────────────── 1. **Observe First** - *Always* invoke \`computer_screenshot\` before your first action **and** whenever the UI may have changed. Screenshot before every action when filling out forms. Never act blindly. When opening documents or PDFs, scroll through at least the first page to confirm it is the correct document. 2. **Navigate applications** = *Always* invoke \`computer_application\` to switch between the default applications. 3. **Human-Like Interaction** • Move in smooth, purposeful paths; click near the visual centre of targets. • Double-click desktop icons to open them. • Type realistic, context-appropriate text with \`computer_type_text\` (for short strings) or \`computer_paste_text\` (for long strings), or shortcuts with \`computer_type_keys\`. 4. **Valid Keys Only** - Use **exactly** the identifiers listed in **VALID KEYS** below when supplying \`keys\` to \`computer_type_keys\` or \`computer_press_keys\`. All identifiers come from nut-tree's \`Key\` enum; they are case-sensitive and contain *no spaces*. 5. **Verify Every Step** - After each action: a. Take another screenshot. b. Confirm the expected state before continuing. If it failed, retry sensibly (try again, and then try 2 different methods). 6. **Efficiency & Clarity** - Combine related key presses; prefer scrolling or dragging over many small moves; minimise unnecessary waits. 7. **Stay Within Scope** - Do nothing the user didn't request; don't suggest unrelated tasks. For form and login fields, don't fill in random data, unless explicitly told to do so. 8. **Security** - If you see a password, secret key, or other sensitive information (or the user shares it with you), do not repeat it in conversation. When typing sensitive information, use \`computer_type_text\` with \`isSensitive\` set to \`true\`. 9. **Consistency & Persistence** - Even if the task is repetitive, do not end the task until the user's goal is completely met. For bulk operations, maintain focus and continue until all items are processed. ──────────────────────── REPETITIVE TASK HANDLING ──────────────────────── When performing repetitive tasks (e.g., "visit each profile", "process all items"): 1. **Track Progress** - Maintain a mental count of: • Total items to process (if known) • Items completed so far • Current item being processed • Any errors encountered 2. **Batch Processing** - For large sets: • Process in groups of 10-20 items • Take brief pauses between batches to prevent system overload • Continue until ALL items are processed 3. **Error Recovery** - If an item fails: • Note the error but continue with the next item • Keep a list of failed items to report at the end • Don't let one failure stop the entire operation 4. **Progress Updates** - Every 10-20 items: • Brief status: "Processed 20/100 profiles, continuing..." • No need for detailed reports unless requested 5. **Completion Criteria** - The task is NOT complete until: • All items in the set are processed, OR • You reach a clear endpoint (e.g., "No more profiles to load"), OR • The user explicitly tells you to stop 6. **State Management** - If the task might span multiple tabs/pages: • Save progress to a file periodically • Include timestamps and item identifiers ──────────────────────── TASK LIFECYCLE TEMPLATE ──────────────────────── 1. **Prepare** - Initial screenshot → plan → estimate scope if possible. 2. **Execute Loop** - For each sub-goal: Screenshot → Think → Act → Verify. 3. **Batch Loop** - For repetitive tasks: • While items remain: - Process batch of 10-20 items - Update progress counter - Check for stop conditions - Brief status update • Continue until ALL done 4. **Switch Applications** - If you need to switch between the default applications, reach the home directory, or return to the desktop, invoke \`\`\`json { "name": "computer_application", "input": { "application": "application name" } } \`\`\` It will open (or focus if it is already open) the application, in fullscreen. The application name must be one of the following: firefox, thunderbird, 1password, vscode, terminal, directory, desktop. 5. **Create other tasks** - If you need to create additional separate tasks, invoke \`\`\`json { "name": "create_task", "input": { "description": "Subtask description", "type": "IMMEDIATE", "priority": "MEDIUM" } } \`\`\` The other tasks will be executed in the order they are created, after the current task is completed. Only create separate tasks if they are not related to the current task. 6. **Schedule future tasks** - If you need to schedule a task to run in the future, invoke \`\`\`json { "name": "create_task", "input": { "description": "Subtask description", "type": "SCHEDULED", "scheduledFor": , "priority": "MEDIUM" } } \`\`\` Only schedule tasks if they must be run in the future. Do not schedule tasks that can be run immediately. 7. **Read Files** - If you need to read file contents, invoke \`\`\`json { "name": "computer_read_file", "input": { "path": "/path/to/file" } } \`\`\` This tool reads files and returns them as document content blocks with base64 data, supporting various file types including documents (PDF, DOCX, TXT, etc.) and images (PNG, JPG, etc.). 8. **Cleanup** - When the user's goal is met: • Close every window, file, or app you opened so the desktop is tidy. • Return to an idle desktop/background. **IMPORTANT**: For bulk operations like "visit each profile in the directory": - Do NOT mark as completed after just a few profiles - Continue until you've processed ALL profiles or reached a clear end - If there are 100+ profiles, process them ALL - Only stop when explicitly told or when there are genuinely no more items ──────────────────────── VALID KEYS ──────────────────────── A, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp, B, Backslash, Backspace, C, CapsLock, Clear, Comma, D, Decimal, Delete, Divide, Down, E, End, Enter, Equal, Escape, F, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, Fn, G, Grave, H, Home, I, Insert, J, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin, M, Menu, Minus, Multiply, N, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock, NumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9, O, P, PageDown, PageUp, Pause, Period, Print, Q, Quote, R, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin, S, ScrollLock, Semicolon, Slash, Space, Subtract, T, Tab, U, Up, V, W, X, Y, Z Remember: **accuracy over speed, clarity and consistency over cleverness**. **For repetitive tasks**: Persistence is key. Continue until ALL items are processed, not just the first few. `; ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.module.ts ================================================ import { Module } from '@nestjs/common'; import { TasksModule } from '../tasks/tasks.module'; import { MessagesModule } from '../messages/messages.module'; import { AgentProcessor } from './agent.processor'; import { ConfigModule } from '@nestjs/config'; import { AgentScheduler } from './agent.scheduler'; import { InputCaptureService } from './input-capture.service'; import { AgentAnalyticsService } from './agent.analytics'; @Module({ imports: [ConfigModule, TasksModule, MessagesModule], providers: [ AgentProcessor, AgentScheduler, InputCaptureService, AgentAnalyticsService, ], exports: [AgentProcessor], }) export class AgentModule {} ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.processor.ts ================================================ import { TasksService } from '../tasks/tasks.service'; import { MessagesService } from '../messages/messages.service'; import { Injectable, Logger } from '@nestjs/common'; import { Message, Role, Task, TaskPriority, TaskStatus, TaskType, } from '@prisma/client'; import { isComputerToolUseContentBlock, isSetTaskStatusToolUseBlock, isCreateTaskToolUseBlock, SetTaskStatusToolUseBlock, RedactedThinkingContentBlock, ThinkingContentBlock, ToolUseContentBlock, } from '@bytebot/shared'; import { MessageContentBlock, MessageContentType, ToolResultContentBlock, TextContentBlock, } from '@bytebot/shared'; import { InputCaptureService } from './input-capture.service'; import { OnEvent } from '@nestjs/event-emitter'; import { BytebotAgentModel, BytebotAgentService, BytebotAgentResponse, } from './agent.types'; import { AGENT_SYSTEM_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, } from './agent.constants'; import { query } from '@anthropic-ai/claude-code'; import Anthropic from '@anthropic-ai/sdk'; @Injectable() export class AgentProcessor { private readonly logger = new Logger(AgentProcessor.name); private currentTaskId: string | null = null; private isProcessing = false; private abortController: AbortController | null = null; private readonly BYTEBOT_DESKTOP_BASE_URL = process.env .BYTEBOT_DESKTOP_BASE_URL as string; constructor( private readonly tasksService: TasksService, private readonly messagesService: MessagesService, private readonly inputCaptureService: InputCaptureService, ) { this.logger.log('AgentProcessor initialized'); } /** * Check if the processor is currently processing a task */ isRunning(): boolean { return this.isProcessing; } /** * Get the current task ID being processed */ getCurrentTaskId(): string | null { return this.currentTaskId; } @OnEvent('task.takeover') handleTaskTakeover({ taskId }: { taskId: string }) { this.logger.log(`Task takeover event received for task ID: ${taskId}`); // If the agent is still processing this task, abort any in-flight operations if (this.currentTaskId === taskId && this.isProcessing) { this.abortController?.abort(); } // Always start capturing user input so that emitted actions are received this.inputCaptureService.start(taskId); } @OnEvent('task.resume') handleTaskResume({ taskId }: { taskId: string }) { if (this.currentTaskId === taskId && this.isProcessing) { this.logger.log(`Task resume event received for task ID: ${taskId}`); this.abortController = new AbortController(); void this.runIteration(taskId); } } @OnEvent('task.cancel') async handleTaskCancel({ taskId }: { taskId: string }) { this.logger.log(`Task cancel event received for task ID: ${taskId}`); await this.stopProcessing(); } processTask(taskId: string) { this.logger.log(`Starting processing for task ID: ${taskId}`); if (this.isProcessing) { this.logger.warn('AgentProcessor is already processing another task'); return; } this.isProcessing = true; this.currentTaskId = taskId; this.abortController = new AbortController(); // Kick off the first iteration without blocking the caller void this.runIteration(taskId); } /** * Convert Anthropic's response content to our MessageContentBlock format */ private formatAnthropicResponse( content: Anthropic.ContentBlock[], ): MessageContentBlock[] { // filter out tool_use blocks that aren't computer tool uses content = content.filter( (block) => block.type !== 'tool_use' || block.name.startsWith('mcp__desktop__'), ); return content.map((block) => { switch (block.type) { case 'text': return { type: MessageContentType.Text, text: block.text, } as TextContentBlock; case 'tool_use': return { type: MessageContentType.ToolUse, id: block.id, name: block.name.replace('mcp__desktop__', ''), input: block.input, } as ToolUseContentBlock; case 'thinking': return { type: MessageContentType.Thinking, thinking: block.thinking, signature: block.signature, } as ThinkingContentBlock; case 'redacted_thinking': return { type: MessageContentType.RedactedThinking, data: block.data, } as RedactedThinkingContentBlock; } }); } /** * Runs a single iteration of task processing and schedules the next * iteration via setImmediate while the task remains RUNNING. */ private async runIteration(taskId: string): Promise { if (!this.isProcessing) { return; } try { const task: Task = await this.tasksService.findById(taskId); if (task.status !== TaskStatus.RUNNING) { this.logger.log( `Task processing completed for task ID: ${taskId} with status: ${task.status}`, ); this.isProcessing = false; this.currentTaskId = null; return; } this.logger.log(`Processing iteration for task ID: ${taskId}`); // Refresh abort controller for this iteration to avoid accumulating // "abort" listeners on a single AbortSignal across iterations. this.abortController = new AbortController(); for await (const message of query({ prompt: task.description, options: { abortController: this.abortController, appendSystemPrompt: AGENT_SYSTEM_PROMPT, permissionMode: 'bypassPermissions', mcpServers: { desktop: { type: 'sse', url: `${this.BYTEBOT_DESKTOP_BASE_URL}/mcp`, }, }, }, })) { let messageContentBlocks: MessageContentBlock[] = []; let role: Role = Role.ASSISTANT; switch (message.type) { case 'user': { if (Array.isArray(message.message.content)) { messageContentBlocks = message.message .content as MessageContentBlock[]; } else if (typeof message.message.content === 'string') { messageContentBlocks = [ { type: MessageContentType.Text, text: message.message.content, } as TextContentBlock, ]; } role = Role.USER; break; } case 'assistant': { messageContentBlocks = this.formatAnthropicResponse( message.message.content, ); break; } case 'system': break; case 'result': { switch (message.subtype) { case 'success': await this.tasksService.update(taskId, { status: TaskStatus.COMPLETED, completedAt: new Date(), }); break; case 'error_max_turns': case 'error_during_execution': await this.tasksService.update(taskId, { status: TaskStatus.NEEDS_HELP, }); break; } break; } } this.logger.debug( `Received ${messageContentBlocks.length} content blocks from LLM`, ); if (messageContentBlocks.length > 0) { await this.messagesService.create({ content: messageContentBlocks, role, taskId, }); } } } catch (error: any) { if (error?.message === 'Claude Code process aborted by user') { this.logger.warn(`Processing aborted for task ID: ${taskId}`); } else { this.logger.error( `Error during task processing iteration for task ID: ${taskId} - ${error.message}`, error.stack, ); await this.tasksService.update(taskId, { status: TaskStatus.FAILED, }); this.isProcessing = false; this.currentTaskId = null; } } } async stopProcessing(): Promise { if (!this.isProcessing) { return; } this.logger.log(`Stopping execution of task ${this.currentTaskId}`); // Signal any in-flight async operations to abort this.abortController?.abort(); await this.inputCaptureService.stop(); this.isProcessing = false; this.currentTaskId = null; } } ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.scheduler.ts ================================================ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { TasksService } from '../tasks/tasks.service'; import { AgentProcessor } from './agent.processor'; import { TaskStatus } from '@prisma/client'; import { writeFile } from './agent.computer-use'; @Injectable() export class AgentScheduler implements OnModuleInit { private readonly logger = new Logger(AgentScheduler.name); constructor( private readonly tasksService: TasksService, private readonly agentProcessor: AgentProcessor, ) {} async onModuleInit() { this.logger.log('AgentScheduler initialized'); await this.handleCron(); } @Cron(CronExpression.EVERY_5_SECONDS) async handleCron() { const now = new Date(); const scheduledTasks = await this.tasksService.findScheduledTasks(); for (const scheduledTask of scheduledTasks) { if (scheduledTask.scheduledFor && scheduledTask.scheduledFor < now) { this.logger.debug( `Task ID: ${scheduledTask.id} is scheduled for ${scheduledTask.scheduledFor}, queuing it`, ); await this.tasksService.update(scheduledTask.id, { queuedAt: now, }); } } if (this.agentProcessor.isRunning()) { return; } // Find the highest priority task to execute const task = await this.tasksService.findNextTask(); if (task) { if (task.files.length > 0) { this.logger.debug( `Task ID: ${task.id} has files, writing them to the desktop`, ); for (const file of task.files) { await writeFile({ path: `/home/user/Desktop/${file.name}`, content: file.data, // file.data is already base64 encoded in the database }); } } await this.tasksService.update(task.id, { status: TaskStatus.RUNNING, executedAt: new Date(), }); this.logger.debug(`Processing task ID: ${task.id}`); this.agentProcessor.processTask(task.id); } } } ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.tools.ts ================================================ /** * Common schema definitions for reuse */ const coordinateSchema = { type: 'object' as const, properties: { x: { type: 'number' as const, description: 'The x-coordinate', }, y: { type: 'number' as const, description: 'The y-coordinate', }, }, required: ['x', 'y'], }; const holdKeysSchema = { type: 'array' as const, items: { type: 'string' as const }, description: 'Optional array of keys to hold during the action', nullable: true, }; const buttonSchema = { type: 'string' as const, enum: ['left', 'right', 'middle'], description: 'The mouse button', }; /** * Tool definitions for mouse actions */ export const _moveMouseTool = { name: 'computer_move_mouse', description: 'Moves the mouse cursor to the specified coordinates', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Target coordinates for mouse movement', }, }, required: ['coordinates'], }, }; export const _traceMouseTool = { name: 'computer_trace_mouse', description: 'Moves the mouse cursor along a specified path of coordinates', input_schema: { type: 'object' as const, properties: { path: { type: 'array' as const, items: coordinateSchema, description: 'Array of coordinate objects representing the path', }, holdKeys: holdKeysSchema, }, required: ['path'], }, }; export const _clickMouseTool = { name: 'computer_click_mouse', description: 'Performs a mouse click at the specified coordinates or current position', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Optional click coordinates (defaults to current position)', nullable: true, }, button: buttonSchema, holdKeys: holdKeysSchema, clickCount: { type: 'integer' as const, description: 'Number of clicks to perform (e.g., 2 for double-click)', default: 1, }, }, required: ['button', 'clickCount'], }, }; export const _pressMouseTool = { name: 'computer_press_mouse', description: 'Presses or releases a specified mouse button', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Optional coordinates (defaults to current position)', nullable: true, }, button: buttonSchema, press: { type: 'string' as const, enum: ['up', 'down'], description: 'Whether to press down or release up', }, }, required: ['button', 'press'], }, }; export const _dragMouseTool = { name: 'computer_drag_mouse', description: 'Drags the mouse along a path while holding a button', input_schema: { type: 'object' as const, properties: { path: { type: 'array' as const, items: coordinateSchema, description: 'Array of coordinates representing the drag path', }, button: buttonSchema, holdKeys: holdKeysSchema, }, required: ['path', 'button'], }, }; export const _scrollTool = { name: 'computer_scroll', description: 'Scrolls the mouse wheel in the specified direction', input_schema: { type: 'object' as const, properties: { coordinates: { ...coordinateSchema, description: 'Coordinates where the scroll should occur', }, direction: { type: 'string' as const, enum: ['up', 'down', 'left', 'right'], description: 'The direction to scroll', }, scrollCount: { type: 'integer' as const, description: 'Number of scroll steps', }, holdKeys: holdKeysSchema, }, required: ['coordinates', 'direction', 'scrollCount'], }, }; /** * Tool definitions for keyboard actions */ export const _typeKeysTool = { name: 'computer_type_keys', description: 'Types a sequence of keys (useful for keyboard shortcuts)', input_schema: { type: 'object' as const, properties: { keys: { type: 'array' as const, items: { type: 'string' as const }, description: 'Array of key names to type in sequence', }, delay: { type: 'number' as const, description: 'Optional delay in milliseconds between key presses', nullable: true, }, }, required: ['keys'], }, }; export const _pressKeysTool = { name: 'computer_press_keys', description: 'Presses or releases specific keys (useful for holding modifiers)', input_schema: { type: 'object' as const, properties: { keys: { type: 'array' as const, items: { type: 'string' as const }, description: 'Array of key names to press or release', }, press: { type: 'string' as const, enum: ['up', 'down'], description: 'Whether to press down or release up', }, }, required: ['keys', 'press'], }, }; export const _typeTextTool = { name: 'computer_type_text', description: 'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.', input_schema: { type: 'object' as const, properties: { text: { type: 'string' as const, description: 'The text string to type', }, delay: { type: 'number' as const, description: 'Optional delay in milliseconds between characters', nullable: true, }, isSensitive: { type: 'boolean' as const, description: 'Flag to indicate sensitive information', nullable: true, }, }, required: ['text'], }, }; export const _pasteTextTool = { name: 'computer_paste_text', description: 'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.', input_schema: { type: 'object' as const, properties: { text: { type: 'string' as const, description: 'The text string to type', }, isSensitive: { type: 'boolean' as const, description: 'Flag to indicate sensitive information', nullable: true, }, }, required: ['text'], }, }; /** * Tool definitions for utility actions */ export const _waitTool = { name: 'computer_wait', description: 'Pauses execution for a specified duration', input_schema: { type: 'object' as const, properties: { duration: { type: 'integer' as const, enum: [500], description: 'The duration to wait in milliseconds', }, }, required: ['duration'], }, }; export const _screenshotTool = { name: 'computer_screenshot', description: 'Captures a screenshot of the current screen', input_schema: { type: 'object' as const, properties: {}, }, }; export const _cursorPositionTool = { name: 'computer_cursor_position', description: 'Gets the current (x, y) coordinates of the mouse cursor', input_schema: { type: 'object' as const, properties: {}, }, }; export const _applicationTool = { name: 'computer_application', description: 'Opens or focuses an application and ensures it is fullscreen', input_schema: { type: 'object' as const, properties: { application: { type: 'string' as const, enum: [ 'firefox', '1password', 'thunderbird', 'vscode', 'terminal', 'desktop', 'directory', ], description: 'The application to open or focus', }, }, required: ['application'], }, }; /** * Tool definitions for task management */ export const _setTaskStatusTool = { name: 'set_task_status', description: 'Sets the status of the current task', input_schema: { type: 'object' as const, properties: { status: { type: 'string' as const, enum: ['completed', 'needs_help'], description: 'The status of the task', }, description: { type: 'string' as const, description: 'If the task is completed, a summary of the task. If the task needs help, a description of the issue or clarification needed.', }, }, required: ['status', 'description'], }, }; export const _createTaskTool = { name: 'create_task', description: 'Creates a new task', input_schema: { type: 'object' as const, properties: { description: { type: 'string' as const, description: 'The description of the task', }, type: { type: 'string' as const, enum: ['IMMEDIATE', 'SCHEDULED'], description: 'The type of the task (defaults to IMMEDIATE)', }, scheduledFor: { type: 'string' as const, format: 'date-time', description: 'RFC 3339 / ISO 8601 datetime for scheduled tasks', }, priority: { type: 'string' as const, enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'], description: 'The priority of the task (defaults to MEDIUM)', }, }, required: ['description'], }, }; /** * Tool definition for reading files */ export const _readFileTool = { name: 'computer_read_file', description: 'Reads a file from the specified path and returns it as a document content block with base64 encoded data', input_schema: { type: 'object' as const, properties: { path: { type: 'string' as const, description: 'The file path to read from', }, }, required: ['path'], }, }; /** * Export all tools as an array */ export const agentTools = [ _moveMouseTool, _traceMouseTool, _clickMouseTool, _pressMouseTool, _dragMouseTool, _scrollTool, _typeKeysTool, _pressKeysTool, _typeTextTool, _pasteTextTool, _waitTool, _screenshotTool, _applicationTool, _cursorPositionTool, _setTaskStatusTool, _createTaskTool, _readFileTool, ]; ================================================ FILE: packages/bytebot-agent-cc/src/agent/agent.types.ts ================================================ import { Message } from '@prisma/client'; import { MessageContentBlock } from '@bytebot/shared'; export interface BytebotAgentResponse { contentBlocks: MessageContentBlock[]; tokenUsage: { inputTokens: number; outputTokens: number; totalTokens: number; }; } export interface BytebotAgentService { generateMessage( systemPrompt: string, messages: Message[], model: string, useTools: boolean, signal?: AbortSignal, ): Promise; } export interface BytebotAgentModel { provider: 'anthropic' | 'openai' | 'google' | 'proxy'; name: string; title: string; contextWindow?: number; } export class BytebotAgentInterrupt extends Error { constructor() { super('BytebotAgentInterrupt'); this.name = 'BytebotAgentInterrupt'; } } ================================================ FILE: packages/bytebot-agent-cc/src/agent/input-capture.service.ts ================================================ import { Injectable, Logger } from '@nestjs/common'; import { io, Socket } from 'socket.io-client'; import { randomUUID } from 'crypto'; import { convertClickMouseActionToToolUseBlock, convertDragMouseActionToToolUseBlock, convertPressKeysActionToToolUseBlock, convertPressMouseActionToToolUseBlock, convertScrollActionToToolUseBlock, convertTypeKeysActionToToolUseBlock, convertTypeTextActionToToolUseBlock, ImageContentBlock, MessageContentBlock, MessageContentType, ScreenshotToolUseBlock, ToolResultContentBlock, UserActionContentBlock, } from '@bytebot/shared'; import { Role } from '@prisma/client'; import { MessagesService } from '../messages/messages.service'; import { ConfigService } from '@nestjs/config'; @Injectable() export class InputCaptureService { private readonly logger = new Logger(InputCaptureService.name); private socket: Socket | null = null; private capturing = false; constructor( private readonly messagesService: MessagesService, private readonly configService: ConfigService, ) {} isCapturing() { return this.capturing; } start(taskId: string) { if (this.socket?.connected && this.capturing) return; if (this.socket && !this.socket.connected) { this.socket.connect(); return; } const baseUrl = this.configService.get('BYTEBOT_DESKTOP_BASE_URL'); if (!baseUrl) { this.logger.warn('BYTEBOT_DESKTOP_BASE_URL missing.'); return; } this.socket = io(baseUrl, { transports: ['websocket'] }); this.socket.on('connect', () => { this.logger.log('Input socket connected'); this.capturing = true; }); this.socket.on( 'screenshotAndAction', async (shot: { image: string }, action: any) => { if (!this.capturing || !taskId) return; // The gateway only sends a click_mouse or drag_mouse action together with screenshots for now. if (action.action !== 'click_mouse' && action.action !== 'drag_mouse') return; const userActionBlock: UserActionContentBlock = { type: MessageContentType.UserAction, content: [ { type: MessageContentType.Image, source: { data: shot.image, media_type: 'image/png', type: 'base64', }, }, ], }; const toolUseId = randomUUID(); switch (action.action) { case 'drag_mouse': userActionBlock.content.push( convertDragMouseActionToToolUseBlock(action, toolUseId), ); break; case 'click_mouse': userActionBlock.content.push( convertClickMouseActionToToolUseBlock(action, toolUseId), ); break; } await this.messagesService.create({ content: [userActionBlock], role: Role.USER, taskId, }); }, ); this.socket.on('action', async (action: any) => { if (!this.capturing || !taskId) return; const toolUseId = randomUUID(); const userActionBlock: UserActionContentBlock = { type: MessageContentType.UserAction, content: [], }; switch (action.action) { case 'drag_mouse': userActionBlock.content.push( convertDragMouseActionToToolUseBlock(action, toolUseId), ); break; case 'press_mouse': userActionBlock.content.push( convertPressMouseActionToToolUseBlock(action, toolUseId), ); break; case 'type_keys': userActionBlock.content.push( convertTypeKeysActionToToolUseBlock(action, toolUseId), ); break; case 'press_keys': userActionBlock.content.push( convertPressKeysActionToToolUseBlock(action, toolUseId), ); break; case 'type_text': userActionBlock.content.push( convertTypeTextActionToToolUseBlock(action, toolUseId), ); break; case 'scroll': userActionBlock.content.push( convertScrollActionToToolUseBlock(action, toolUseId), ); break; default: this.logger.warn(`Unknown action ${action.action}`); } if (userActionBlock.content.length > 0) { await this.messagesService.create({ content: [userActionBlock], role: Role.USER, taskId, }); } }); this.socket.on('disconnect', () => { this.logger.log('Input socket disconnected'); this.capturing = false; }); } async stop() { if (!this.socket) return; if (this.socket.connected) this.socket.disconnect(); else this.socket.removeAllListeners(); this.socket = null; this.capturing = false; } } ================================================ FILE: packages/bytebot-agent-cc/src/app.controller.ts ================================================ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } } ================================================ FILE: packages/bytebot-agent-cc/src/app.module.ts ================================================ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AgentModule } from './agent/agent.module'; import { TasksModule } from './tasks/tasks.module'; import { MessagesModule } from './messages/messages.module'; import { PrismaModule } from './prisma/prisma.module'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ imports: [ ScheduleModule.forRoot(), EventEmitterModule.forRoot(), ConfigModule.forRoot({ isGlobal: true, }), AgentModule, TasksModule, MessagesModule, PrismaModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {} ================================================ FILE: packages/bytebot-agent-cc/src/app.service.ts ================================================ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } } ================================================ FILE: packages/bytebot-agent-cc/src/main.ts ================================================ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { webcrypto } from 'crypto'; import { json, urlencoded } from 'express'; // Polyfill for crypto global (required by @nestjs/schedule) if (!globalThis.crypto) { globalThis.crypto = webcrypto as any; } async function bootstrap() { console.log('Starting bytebot-agent application...'); try { const app = await NestFactory.create(AppModule); // Configure body parser with increased payload size limit (50MB) app.use(json({ limit: '50mb' })); app.use(urlencoded({ limit: '50mb', extended: true })); // Set global prefix for all routes app.setGlobalPrefix('api'); // Enable CORS app.enableCors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], }); await app.listen(process.env.PORT ?? 9991); } catch (error) { console.error('Error starting application:', error); } } bootstrap(); ================================================ FILE: packages/bytebot-agent-cc/src/messages/messages.module.ts ================================================ import { Module, forwardRef } from '@nestjs/common'; import { MessagesService } from './messages.service'; import { PrismaModule } from '../prisma/prisma.module'; import { TasksModule } from '../tasks/tasks.module'; @Module({ imports: [PrismaModule, forwardRef(() => TasksModule)], providers: [MessagesService], exports: [MessagesService], }) export class MessagesModule {} ================================================ FILE: packages/bytebot-agent-cc/src/messages/messages.service.ts ================================================ import { Injectable, NotFoundException, Inject, forwardRef, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { Message, Role, Prisma } from '@prisma/client'; import { MessageContentBlock, isComputerToolUseContentBlock, isToolResultContentBlock, isUserActionContentBlock, } from '@bytebot/shared'; import { TasksGateway } from '../tasks/tasks.gateway'; // Extended message type for processing export interface ProcessedMessage extends Message { take_over?: boolean; } export interface GroupedMessages { role: Role; messages: ProcessedMessage[]; take_over?: boolean; } @Injectable() export class MessagesService { constructor( private prisma: PrismaService, @Inject(forwardRef(() => TasksGateway)) private readonly tasksGateway: TasksGateway, ) {} async create(data: { content: MessageContentBlock[]; role: Role; taskId: string; }): Promise { const message = await this.prisma.message.create({ data: { content: data.content as Prisma.InputJsonValue, role: data.role, taskId: data.taskId, }, }); this.tasksGateway.emitNewMessage(data.taskId, message); return message; } async findEvery(taskId: string): Promise { return this.prisma.message.findMany({ where: { taskId, }, orderBy: { createdAt: 'asc', }, }); } async findAll( taskId: string, options?: { limit?: number; page?: number; }, ): Promise { const { limit = 10, page = 1 } = options || {}; // Calculate offset based on page and limit const offset = (page - 1) * limit; return this.prisma.message.findMany({ where: { taskId, }, orderBy: { createdAt: 'asc', }, take: limit, skip: offset, }); } async findUnsummarized(taskId: string): Promise { return this.prisma.message.findMany({ where: { taskId, // find messages that don't have a summaryId summaryId: null, }, orderBy: { createdAt: 'asc' }, }); } async attachSummary( taskId: string, summaryId: string, messageIds: string[], ): Promise { if (messageIds.length === 0) { return; } await this.prisma.message.updateMany({ where: { taskId, id: { in: messageIds } }, data: { summaryId }, }); } /** * Groups back-to-back messages from the same role and take_over status */ private groupBackToBackMessages( messages: ProcessedMessage[], ): GroupedMessages[] { const groupedConversation: GroupedMessages[] = []; let currentGroup: GroupedMessages | null = null; for (const message of messages) { const role = message.role; const isTakeOver = message.take_over || false; // If this is the first message, role is different, or take_over status is different from the previous group if ( !currentGroup || currentGroup.role !== role || currentGroup.take_over !== isTakeOver ) { // Save the previous group if it exists if (currentGroup) { groupedConversation.push(currentGroup); } // Start a new group currentGroup = { role: role, messages: [message], take_over: isTakeOver, }; } else { // Same role and take_over status as previous, merge the content currentGroup.messages.push(message); } } // Add the last group if (currentGroup) { groupedConversation.push(currentGroup); } return groupedConversation; } /** * Filters and processes messages, adding take_over flags where appropriate * Only text messages from the user should appear as user messages * Computer tool use messages should be shown as assistant messages with take_over flag */ private filterMessages(messages: Message[]): ProcessedMessage[] { const filteredMessages: ProcessedMessage[] = []; for (const message of messages) { const processedMessage: ProcessedMessage = { ...message }; const contentBlocks = message.content as MessageContentBlock[]; // If the role is a user message and all the content blocks are tool result blocks or they are take over actions if (message.role === Role.USER) { if (contentBlocks.every((block) => isToolResultContentBlock(block))) { // Pure tool results should be shown as assistant messages processedMessage.role = Role.ASSISTANT; } else if ( contentBlocks.every((block) => isUserActionContentBlock(block)) ) { // Extract computer tool use (take over actions) from the user action content blocks and show them as assistant messages with take_over flag processedMessage.content = contentBlocks .flatMap((block) => { return block.content; }) .filter((block) => isComputerToolUseContentBlock(block)); processedMessage.role = Role.ASSISTANT; processedMessage.take_over = true; } // If there are text blocks mixed with tool blocks, keep as user message // Only pure text messages from user should remain as user messages } filteredMessages.push(processedMessage); } return filteredMessages; } /** * Returns raw messages without any processing */ async findRawMessages( taskId: string, options?: { limit?: number; page?: number; }, ): Promise { return this.findAll(taskId, options); } /** * Returns processed and grouped messages for the chat UI */ async findProcessedMessages( taskId: string, options?: { limit?: number; page?: number; }, ): Promise { const messages = await this.findAll(taskId, options); const filteredMessages = this.filterMessages(messages); return this.groupBackToBackMessages(filteredMessages); } } ================================================ FILE: packages/bytebot-agent-cc/src/prisma/prisma.module.ts ================================================ import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ================================================ FILE: packages/bytebot-agent-cc/src/prisma/prisma.service.ts ================================================ import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { constructor() { super(); } async onModuleInit() { await this.$connect(); } } ================================================ FILE: packages/bytebot-agent-cc/src/tasks/dto/add-task-message.dto.ts ================================================ import { IsNotEmpty, IsString } from 'class-validator'; export class AddTaskMessageDto { @IsNotEmpty() @IsString() message: string; } ================================================ FILE: packages/bytebot-agent-cc/src/tasks/dto/create-task.dto.ts ================================================ import { IsArray, IsDate, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; import { Role, TaskPriority, TaskType } from '@prisma/client'; export class TaskFileDto { @IsNotEmpty() @IsString() name: string; @IsNotEmpty() @IsString() base64: string; @IsNotEmpty() @IsString() type: string; @IsNotEmpty() @IsNumber() size: number; } export class CreateTaskDto { @IsNotEmpty() @IsString() description: string; @IsOptional() @IsString() type?: TaskType; @IsOptional() @IsDate() scheduledFor?: Date; @IsOptional() @IsString() priority?: TaskPriority; @IsOptional() @IsString() createdBy?: Role; @IsOptional() model?: any; @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => TaskFileDto) files?: TaskFileDto[]; } ================================================ FILE: packages/bytebot-agent-cc/src/tasks/dto/update-task.dto.ts ================================================ import { IsEnum, IsOptional } from 'class-validator'; import { TaskPriority, TaskStatus } from '@prisma/client'; export class UpdateTaskDto { @IsOptional() @IsEnum(TaskStatus) status?: TaskStatus; @IsOptional() @IsEnum(TaskPriority) priority?: TaskPriority; @IsOptional() queuedAt?: Date; @IsOptional() executedAt?: Date; @IsOptional() completedAt?: Date; } ================================================ FILE: packages/bytebot-agent-cc/src/tasks/tasks.controller.ts ================================================ import { Controller, Get, Post, Body, Param, Delete, HttpStatus, HttpCode, Query, } from '@nestjs/common'; import { TasksService } from './tasks.service'; import { CreateTaskDto } from './dto/create-task.dto'; import { Message, Task } from '@prisma/client'; import { AddTaskMessageDto } from './dto/add-task-message.dto'; import { MessagesService } from '../messages/messages.service'; import { BytebotAgentModel } from 'src/agent/agent.types'; @Controller('tasks') export class TasksController { constructor( private readonly tasksService: TasksService, private readonly messagesService: MessagesService, ) {} @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createTaskDto: CreateTaskDto): Promise { return this.tasksService.create(createTaskDto); } @Get() async findAll( @Query('page') page?: string, @Query('limit') limit?: string, @Query('status') status?: string, @Query('statuses') statuses?: string, ): Promise<{ tasks: Task[]; total: number; totalPages: number }> { const pageNum = page ? parseInt(page, 10) : 1; const limitNum = limit ? parseInt(limit, 10) : 10; // Handle both single status and multiple statuses let statusFilter: string[] | undefined; if (statuses) { statusFilter = statuses.split(','); } else if (status) { statusFilter = [status]; } return this.tasksService.findAll(pageNum, limitNum, statusFilter); } @Get('models') async getModels() { return [ { provider: 'anthropic', name: 'claude-code', title: 'Claude Code', contextWindow: 200000, }, ]; } @Get(':id') async findById(@Param('id') id: string): Promise { return this.tasksService.findById(id); } @Get(':id/messages') async taskMessages( @Param('id') taskId: string, @Query('limit') limit?: string, @Query('page') page?: string, ): Promise { const options = { limit: limit ? parseInt(limit, 10) : undefined, page: page ? parseInt(page, 10) : undefined, }; const messages = await this.messagesService.findAll(taskId, options); return messages; } @Post(':id/messages') @HttpCode(HttpStatus.CREATED) async addTaskMessage( @Param('id') taskId: string, @Body() guideTaskDto: AddTaskMessageDto, ): Promise { return this.tasksService.addTaskMessage(taskId, guideTaskDto); } @Get(':id/messages/raw') async taskRawMessages( @Param('id') taskId: string, @Query('limit') limit?: string, @Query('page') page?: string, ): Promise { const options = { limit: limit ? parseInt(limit, 10) : undefined, page: page ? parseInt(page, 10) : undefined, }; return this.messagesService.findRawMessages(taskId, options); } @Get(':id/messages/processed') async taskProcessedMessages( @Param('id') taskId: string, @Query('limit') limit?: string, @Query('page') page?: string, ) { const options = { limit: limit ? parseInt(limit, 10) : undefined, page: page ? parseInt(page, 10) : undefined, }; return this.messagesService.findProcessedMessages(taskId, options); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async delete(@Param('id') id: string): Promise { await this.tasksService.delete(id); } @Post(':id/takeover') @HttpCode(HttpStatus.OK) async takeOver(@Param('id') taskId: string): Promise { return this.tasksService.takeOver(taskId); } @Post(':id/resume') @HttpCode(HttpStatus.OK) async resume(@Param('id') taskId: string): Promise { return this.tasksService.resume(taskId); } @Post(':id/cancel') @HttpCode(HttpStatus.OK) async cancel(@Param('id') taskId: string): Promise { return this.tasksService.cancel(taskId); } } ================================================ FILE: packages/bytebot-agent-cc/src/tasks/tasks.gateway.ts ================================================ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Injectable } from '@nestjs/common'; @Injectable() @WebSocketGateway({ cors: { origin: '*', methods: ['GET', 'POST'], }, }) export class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('join_task') handleJoinTask(client: Socket, taskId: string) { client.join(`task_${taskId}`); console.log(`Client ${client.id} joined task ${taskId}`); } @SubscribeMessage('leave_task') handleLeaveTask(client: Socket, taskId: string) { client.leave(`task_${taskId}`); console.log(`Client ${client.id} left task ${taskId}`); } emitTaskUpdate(taskId: string, task: any) { this.server.to(`task_${taskId}`).emit('task_updated', task); } emitNewMessage(taskId: string, message: any) { this.server.to(`task_${taskId}`).emit('new_message', message); } emitTaskCreated(task: any) { this.server.emit('task_created', task); } emitTaskDeleted(taskId: string) { this.server.emit('task_deleted', taskId); } } ================================================ FILE: packages/bytebot-agent-cc/src/tasks/tasks.module.ts ================================================ import { Module } from '@nestjs/common'; import { TasksController } from './tasks.controller'; import { TasksService } from './tasks.service'; import { TasksGateway } from './tasks.gateway'; import { PrismaModule } from '../prisma/prisma.module'; import { MessagesModule } from '../messages/messages.module'; @Module({ imports: [PrismaModule, MessagesModule], controllers: [TasksController], providers: [TasksService, TasksGateway], exports: [TasksService, TasksGateway], }) export class TasksModule {} ================================================ FILE: packages/bytebot-agent-cc/src/tasks/tasks.service.ts ================================================ import { Injectable, NotFoundException, Logger, BadRequestException, Inject, forwardRef, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateTaskDto } from './dto/create-task.dto'; import { UpdateTaskDto } from './dto/update-task.dto'; import { Task, Role, Prisma, TaskStatus, TaskType, TaskPriority, File, } from '@prisma/client'; import { AddTaskMessageDto } from './dto/add-task-message.dto'; import { TasksGateway } from './tasks.gateway'; import { ConfigService } from '@nestjs/config'; import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class TasksService { private readonly logger = new Logger(TasksService.name); constructor( readonly prisma: PrismaService, @Inject(forwardRef(() => TasksGateway)) private readonly tasksGateway: TasksGateway, private readonly configService: ConfigService, private readonly eventEmitter: EventEmitter2, ) { this.logger.log('TasksService initialized'); } async create(createTaskDto: CreateTaskDto): Promise { this.logger.log( `Creating new task with description: ${createTaskDto.description}`, ); const task = await this.prisma.$transaction(async (prisma) => { // Create the task first this.logger.debug('Creating task record in database'); const task = await prisma.task.create({ data: { description: createTaskDto.description, type: createTaskDto.type || TaskType.IMMEDIATE, priority: createTaskDto.priority || TaskPriority.MEDIUM, status: TaskStatus.PENDING, createdBy: createTaskDto.createdBy || Role.USER, model: createTaskDto.model, ...(createTaskDto.scheduledFor ? { scheduledFor: createTaskDto.scheduledFor } : {}), }, }); this.logger.log(`Task created successfully with ID: ${task.id}`); let filesDescription = ''; // Save files if provided if (createTaskDto.files && createTaskDto.files.length > 0) { this.logger.debug( `Saving ${createTaskDto.files.length} file(s) for task ID: ${task.id}`, ); filesDescription += `\n`; const filePromises = createTaskDto.files.map((file) => { // Extract base64 data without the data URL prefix const base64Data = file.base64.includes('base64,') ? file.base64.split('base64,')[1] : file.base64; filesDescription += `\nFile ${file.name} written to desktop.`; return prisma.file.create({ data: { name: file.name, type: file.type || 'application/octet-stream', size: file.size, data: base64Data, taskId: task.id, }, }); }); await Promise.all(filePromises); this.logger.debug(`Files saved successfully for task ID: ${task.id}`); } // Create the initial system message this.logger.debug(`Creating initial message for task ID: ${task.id}`); await prisma.message.create({ data: { content: [ { type: 'text', text: `${createTaskDto.description} ${filesDescription}`, }, ] as Prisma.InputJsonValue, role: Role.USER, taskId: task.id, }, }); this.logger.debug(`Initial message created for task ID: ${task.id}`); return task; }); this.tasksGateway.emitTaskCreated(task); return task; } async findScheduledTasks(): Promise { return this.prisma.task.findMany({ where: { scheduledFor: { not: null, }, queuedAt: null, }, orderBy: [{ scheduledFor: 'asc' }], }); } async findNextTask(): Promise<(Task & { files: File[] }) | null> { const task = await this.prisma.task.findFirst({ where: { status: { in: [TaskStatus.RUNNING, TaskStatus.PENDING], }, }, orderBy: [ { executedAt: 'asc' }, { priority: 'desc' }, { queuedAt: 'asc' }, { createdAt: 'asc' }, ], include: { files: true, }, }); if (task) { this.logger.log( `Found existing task with ID: ${task.id}, and status ${task.status}. Resuming.`, ); } return task; } async findAll( page = 1, limit = 10, statuses?: string[], ): Promise<{ tasks: Task[]; total: number; totalPages: number }> { this.logger.log( `Retrieving tasks - page: ${page}, limit: ${limit}, statuses: ${statuses?.join(',')}`, ); const skip = (page - 1) * limit; const whereClause: Prisma.TaskWhereInput = statuses && statuses.length > 0 ? { status: { in: statuses as TaskStatus[] } } : {}; const [tasks, total] = await Promise.all([ this.prisma.task.findMany({ where: whereClause, orderBy: { createdAt: 'desc', }, skip, take: limit, }), this.prisma.task.count({ where: whereClause }), ]); const totalPages = Math.ceil(total / limit); this.logger.debug(`Retrieved ${tasks.length} tasks out of ${total} total`); return { tasks, total, totalPages }; } async findById(id: string): Promise { this.logger.log(`Retrieving task by ID: ${id}`); try { const task = await this.prisma.task.findUnique({ where: { id }, include: { files: true, }, }); if (!task) { this.logger.warn(`Task with ID: ${id} not found`); throw new NotFoundException(`Task with ID ${id} not found`); } this.logger.debug(`Retrieved task with ID: ${id}`); return task; } catch (error: any) { this.logger.error(`Error retrieving task ID: ${id} - ${error.message}`); this.logger.error(error.stack); throw error; } } async update(id: string, updateTaskDto: UpdateTaskDto): Promise { this.logger.log(`Updating task with ID: ${id}`); this.logger.debug(`Update data: ${JSON.stringify(updateTaskDto)}`); const existingTask = await this.findById(id); if (!existingTask) { this.logger.warn(`Task with ID: ${id} not found for update`); throw new NotFoundException(`Task with ID ${id} not found`); } let updatedTask = await this.prisma.task.update({ where: { id }, data: updateTaskDto, }); if (updateTaskDto.status === TaskStatus.COMPLETED) { this.eventEmitter.emit('task.completed', { taskId: id }); } else if (updateTaskDto.status === TaskStatus.NEEDS_HELP) { updatedTask = await this.takeOver(id); } else if (updateTaskDto.status === TaskStatus.FAILED) { this.eventEmitter.emit('task.failed', { taskId: id }); } this.logger.log(`Successfully updated task ID: ${id}`); this.logger.debug(`Updated task: ${JSON.stringify(updatedTask)}`); this.tasksGateway.emitTaskUpdate(id, updatedTask); return updatedTask; } async delete(id: string): Promise { this.logger.log(`Deleting task with ID: ${id}`); const deletedTask = await this.prisma.task.delete({ where: { id }, }); this.logger.log(`Successfully deleted task ID: ${id}`); this.tasksGateway.emitTaskDeleted(id); return deletedTask; } async addTaskMessage(taskId: string, addTaskMessageDto: AddTaskMessageDto) { const task = await this.findById(taskId); if (!task) { this.logger.warn(`Task with ID: ${taskId} not found for guiding`); throw new NotFoundException(`Task with ID ${taskId} not found`); } const message = await this.prisma.message.create({ data: { content: [{ type: 'text', text: addTaskMessageDto.message }], role: Role.USER, taskId, }, }); this.tasksGateway.emitNewMessage(taskId, message); return task; } async resume(taskId: string): Promise { this.logger.log(`Resuming task ID: ${taskId}`); const task = await this.findById(taskId); if (!task) { throw new NotFoundException(`Task with ID ${taskId} not found`); } if (task.control !== Role.USER) { throw new BadRequestException(`Task ${taskId} is not under user control`); } const updatedTask = await this.prisma.task.update({ where: { id: taskId }, data: { control: Role.ASSISTANT, status: TaskStatus.RUNNING, }, }); try { await fetch( `${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/stop`, { method: 'POST' }, ); } catch (error) { this.logger.error('Failed to stop input tracking', error); } // Broadcast resume event so AgentProcessor can react this.eventEmitter.emit('task.resume', { taskId }); this.logger.log(`Task ${taskId} resumed`); this.tasksGateway.emitTaskUpdate(taskId, updatedTask); return updatedTask; } async takeOver(taskId: string): Promise { this.logger.log(`Taking over control for task ID: ${taskId}`); const task = await this.findById(taskId); if (!task) { throw new NotFoundException(`Task with ID ${taskId} not found`); } if (task.control !== Role.ASSISTANT) { throw new BadRequestException( `Task ${taskId} is not under agent control`, ); } const updatedTask = await this.prisma.task.update({ where: { id: taskId }, data: { control: Role.USER, }, }); try { await fetch( `${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/start`, { method: 'POST' }, ); } catch (error) { this.logger.error('Failed to start input tracking', error); } // Broadcast takeover event so AgentProcessor can react this.eventEmitter.emit('task.takeover', { taskId }); this.logger.log(`Task ${taskId} takeover initiated`); this.tasksGateway.emitTaskUpdate(taskId, updatedTask); return updatedTask; } async cancel(taskId: string): Promise { this.logger.log(`Cancelling task ID: ${taskId}`); const task = await this.findById(taskId); if (!task) { throw new NotFoundException(`Task with ID ${taskId} not found`); } if ( task.status === TaskStatus.COMPLETED || task.status === TaskStatus.FAILED || task.status === TaskStatus.CANCELLED ) { throw new BadRequestException( `Task ${taskId} is already completed, failed, or cancelled`, ); } const updatedTask = await this.prisma.task.update({ where: { id: taskId }, data: { status: TaskStatus.CANCELLED, }, }); // Broadcast cancel event so AgentProcessor can cancel processing this.eventEmitter.emit('task.cancel', { taskId }); this.logger.log(`Task ${taskId} cancelled and marked as failed`); this.tasksGateway.emitTaskUpdate(taskId, updatedTask); return updatedTask; } } ================================================ FILE: packages/bytebot-agent-cc/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] } ================================================ FILE: packages/bytebot-agent-cc/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "incremental": true, "skipLibCheck": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, "noFallthroughCasesInSwitch": false } } ================================================ FILE: packages/bytebot-llm-proxy/Dockerfile ================================================ FROM ghcr.io/berriai/litellm:main-stable # Add custom config into the image COPY ./bytebot-llm-proxy/litellm-config.yaml /app/config.yaml CMD ["--config", "/app/config.yaml", "--port", "4000"] ================================================ FILE: packages/bytebot-llm-proxy/litellm-config.yaml ================================================ model_list: # Anthropic Models - model_name: claude-opus-4 litellm_params: model: anthropic/claude-opus-4-20250514 api_key: os.environ/ANTHROPIC_API_KEY - model_name: claude-sonnet-4 litellm_params: model: anthropic/claude-sonnet-4-20250514 api_key: os.environ/ANTHROPIC_API_KEY # OpenAI Models - model_name: gpt-4.1 litellm_params: model: openai/gpt-4.1 api_key: os.environ/OPENAI_API_KEY - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY # Gemini Models - model_name: gemini-2.5-pro litellm_params: model: gemini/gemini-2.5-pro api_key: os.environ/GEMINI_API_KEY - model_name: gemini-2.5-flash litellm_params: model: gemini/gemini-2.5-flash api_key: os.environ/GEMINI_API_KEY ================================================ FILE: packages/bytebot-ui/.dockerignore ================================================ **/node_modules **/dist **/.next **/.git **/.vscode **/.env* **/npm-debug.log **/yarn-debug.log **/yarn-error.log **/package-lock.json ================================================ FILE: packages/bytebot-ui/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) .env # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: packages/bytebot-ui/.prettierrc.json ================================================ { "plugins": ["prettier-plugin-tailwindcss"] } ================================================ FILE: packages/bytebot-ui/Dockerfile ================================================ # Base image FROM node:20-alpine # Declare build arguments ARG BYTEBOT_AGENT_BASE_URL ARG BYTEBOT_DESKTOP_VNC_URL # Set environment variables for the build process ENV BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL} ENV BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL} # Create app directory WORKDIR /app # Copy app source COPY ./shared ./shared COPY ./bytebot-ui/ ./bytebot-ui WORKDIR /app/bytebot-ui # Install dependencies RUN npm install RUN npm run build # Run the application CMD ["npm", "run", "start"] ================================================ FILE: packages/bytebot-ui/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "src/app/globals.css", "baseColor": "stone", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "iconLibrary": "lucide" } ================================================ FILE: packages/bytebot-ui/eslint.config.mjs ================================================ import { dirname } from "path"; import { fileURLToPath } from "url"; import { FlatCompat } from "@eslint/eslintrc"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, }); const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), ]; export default eslintConfig; ================================================ FILE: packages/bytebot-ui/next.config.ts ================================================ import type { NextConfig } from "next"; import dotenv from "dotenv"; dotenv.config(); const nextConfig: NextConfig = { transpilePackages: ["@bytebot/shared"], }; export default nextConfig; ================================================ FILE: packages/bytebot-ui/package.json ================================================ { "name": "bytebot-ui", "version": "0.1.0", "private": true, "scripts": { "dev": "npm run build --prefix ../shared && tsx server.ts", "build": "npm run build --prefix ../shared && next build", "start": "npm run build --prefix ../shared && tsx server.ts", "lint": "next lint" }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@bytebot/shared": "../shared", "@hugeicons/core-free-icons": "^1.0.14", "@hugeicons/react": "^1.0.5", "@prisma/client": "^6.5.0", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.11", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@types/express": "^5.0.1", "@types/http-proxy": "^1.17.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dotenv": "^16.4.7", "express": "^4.21.2", "http-proxy": "^1.18.1", "http-proxy-middleware": "^3.0.5", "lucide-react": "^0.517.0", "motion": "^12.12.1", "next": ">=15.4.7", "next-themes": "^0.4.6", "next-transpile-modules": "^10.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", "react-vnc": "^3.1.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^3.0.2", "tsx": "^4.19.3", "tw-animate-css": "^1.2.4", "zod": "^3.24.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.3", "@types/node": "^20.17.27", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.2.3", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", "prisma": "^6.5.0", "tailwindcss": "^4", "typescript": "^5" } } ================================================ FILE: packages/bytebot-ui/postcss.config.mjs ================================================ const config = { plugins: ["@tailwindcss/postcss"], }; export default config; ================================================ FILE: packages/bytebot-ui/server.ts ================================================ import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; import { createProxyServer } from "http-proxy"; import next from "next"; import { createServer } from "http"; import dotenv from "dotenv"; // Load environment variables dotenv.config(); const dev = process.env.NODE_ENV !== "production"; const hostname = process.env.HOSTNAME || "localhost"; const port = parseInt(process.env.PORT || "9992", 10); // Backend URLs const BYTEBOT_AGENT_BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL; const BYTEBOT_DESKTOP_VNC_URL = process.env.BYTEBOT_DESKTOP_VNC_URL; const app = next({ dev, hostname, port }); app .prepare() .then(() => { const handle = app.getRequestHandler(); const nextUpgradeHandler = app.getUpgradeHandler(); const vncProxy = createProxyServer({ changeOrigin: true, ws: true }); const expressApp = express(); const server = createServer(expressApp); // WebSocket proxy for Socket.IO connections to backend const tasksProxy = createProxyMiddleware({ target: BYTEBOT_AGENT_BASE_URL, ws: true, pathRewrite: { "^/api/proxy/tasks": "/socket.io" }, }); // Apply HTTP proxies expressApp.use("/api/proxy/tasks", tasksProxy); expressApp.use("/api/proxy/websockify", (req, res) => { console.log("Proxying websockify request"); // Rewrite path const targetUrl = new URL(BYTEBOT_DESKTOP_VNC_URL!); req.url = targetUrl.pathname + (req.url?.replace(/^\/api\/proxy\/websockify/, "") || ""); vncProxy.web(req, res, { target: `${targetUrl.protocol}//${targetUrl.host}`, }); }); // Handle all other requests with Next.js expressApp.all("*", (req, res) => handle(req, res)); // Properly upgrade WebSocket connections server.on("upgrade", (request, socket, head) => { const { pathname } = new URL( request.url!, `http://${request.headers.host}`, ); if (pathname.startsWith("/api/proxy/tasks")) { return tasksProxy.upgrade(request, socket as any, head); } if (pathname.startsWith("/api/proxy/websockify")) { const targetUrl = new URL(BYTEBOT_DESKTOP_VNC_URL!); request.url = targetUrl.pathname + (request.url?.replace(/^\/api\/proxy\/websockify/, "") || ""); console.log("Proxying websockify upgrade request: ", request.url); return vncProxy.ws(request, socket as any, head, { target: `${targetUrl.protocol}//${targetUrl.host}`, }); } nextUpgradeHandler(request, socket, head); }); server.listen(port, hostname, () => { console.log(`> Ready on http://${hostname}:${port}`); }); }) .catch((err) => { console.error("Server failed to start:", err); process.exit(1); }); ================================================ FILE: packages/bytebot-ui/src/app/api/[[...path]]/route.ts ================================================ import { NextRequest } from "next/server"; /* -------------------------------------------------------------------- */ /* generic proxy helper */ /* -------------------------------------------------------------------- */ async function proxy(req: NextRequest, path: string[]): Promise { const BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL!; const subPath = path.length ? path.join("/") : ""; const url = `${BASE_URL}/${subPath}${req.nextUrl.search}`; // Extract cookies from the incoming request const cookies = req.headers.get("cookie"); const init: RequestInit = { method: req.method, headers: { "Content-Type": "application/json", ...(cookies && { Cookie: cookies }), }, body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.text(), }; const res = await fetch(url, init); const body = await res.text(); // Extract Set-Cookie headers from the backend response const setCookieHeaders = res.headers.getSetCookie?.() || []; // Create response headers const responseHeaders = new Headers({ "Content-Type": "application/json", }); // Add Set-Cookie headers if they exist setCookieHeaders.forEach((cookie) => { responseHeaders.append("Set-Cookie", cookie); }); return new Response(body, { status: res.status, headers: responseHeaders, }); } /* -------------------------------------------------------------------- */ /* route handlers */ /* -------------------------------------------------------------------- */ type PathParams = Promise<{ path?: string[] }>; // <- Promise is the key async function handler(req: NextRequest, { params }: { params: PathParams }) { const { path } = await params; return proxy(req, path ?? []); } export const GET = handler; export const POST = handler; export const PUT = handler; export const PATCH = handler; export const DELETE = handler; export const OPTIONS = handler; export const HEAD = handler; ================================================ FILE: packages/bytebot-ui/src/app/desktop/page.tsx ================================================ "use client"; import React from "react"; import { Header } from "@/components/layout/Header"; import { DesktopContainer } from "@/components/ui/desktop-container"; export default function DesktopPage() { return (
{/* Main container */}
{/* No action buttons for desktop page */}
); } ================================================ FILE: packages/bytebot-ui/src/app/globals.css ================================================ @import "tailwindcss"; @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--color-bytebot-bronze-light-4); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); --color-chart-5: var(--chart-5); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); --color-chart-2: var(--chart-2); --color-chart-1: var(--chart-1); --color-ring: var(--ring); --color-input: var(--input); --color-border: var(--border); --color-destructive: var(--destructive); --color-accent-foreground: var(--accent-foreground); --color-accent: var(--accent); --color-muted-foreground: var(--muted-foreground); --color-muted: var(--muted); --color-secondary-foreground: var(--secondary-foreground); --color-secondary: var(--secondary); --color-primary-foreground: var(--primary-foreground); --color-primary: var(--primary); --color-popover-foreground: var(--popover-foreground); --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); /* Colors */ /* Base */ --color-bytebot-white: rgba(255, 255, 255, 1); --color-bytebot-transparent: rgba(255, 255, 255, 0); --color-bytebot-black: rgba(0, 0, 0, 1); /* Bronze light */ --color-bytebot-bronze-light-1: rgba(251, 249, 249, 1); --color-bytebot-bronze-light-2: rgba(246, 244, 244, 1); --color-bytebot-bronze-light-3: rgba(241, 239, 238, 1); --color-bytebot-bronze-light-4: rgba(236, 233, 232, 1); --color-bytebot-bronze-light-5: rgba(230, 225, 224, 1); --color-bytebot-bronze-light-6: rgba(224, 218, 217, 1); --color-bytebot-bronze-light-7: rgba(218, 212, 210, 1); --color-bytebot-bronze-light-8: rgba(209, 201, 199, 1); --color-bytebot-bronze-light-9: rgba(152, 141, 139, 1); --color-bytebot-bronze-light-10: rgba(141, 130, 128, 1); --color-bytebot-bronze-light-11: rgba(106, 99, 98, 1); --color-bytebot-bronze-light-12: rgba(45, 42, 42, 1); --color-bytebot-bronze-light-a1: rgba(71, 0, 0, 0.02500000037252903); --color-bytebot-bronze-light-a2: rgba(36, 12, 0, 0.04500000178813934); --color-bytebot-bronze-light-a3: rgba(51, 17, 0, 0.06800000369548798); --color-bytebot-bronze-light-a4: rgba(51, 13, 0, 0.09000000357627869); --color-bytebot-bronze-light-a5: rgba(40, 7, 0, 0.11999999731779099); --color-bytebot-bronze-light-a6: rgba(44, 7, 0, 0.15000000596046448); --color-bytebot-bronze-light-a7: rgba(40, 8, 0, 0.17389999330043793); --color-bytebot-bronze-light-a8: rgba(41, 8, 0, 0.2175000011920929); --color-bytebot-bronze-light-a9: rgba(28, 4, 0, 0.45500001311302185); --color-bytebot-bronze-light-a10: rgba(26, 4, 0, 0.49799999594688416); --color-bytebot-bronze-light-a11: rgba(13, 2, 0, 0.6100000143051147); --color-bytebot-bronze-light-a12: rgba(4, 2, 2, 0.8399999737739563); /* Bronze dark */ --color-bytebot-bronze-dark-1: rgba(19, 16, 16, 1); --color-bytebot-bronze-dark-2: rgba(27, 25, 24, 1); --color-bytebot-bronze-dark-3: rgba(37, 34, 33, 1); --color-bytebot-bronze-dark-4: rgba(45, 41, 40, 1); --color-bytebot-bronze-dark-5: rgba(53, 48, 47, 1); --color-bytebot-bronze-dark-6: rgba(57, 51, 50, 1); --color-bytebot-bronze-dark-7: rgba(77, 70, 69, 1); --color-bytebot-bronze-dark-8: rgba(103, 94, 92, 1); --color-bytebot-bronze-dark-9: rgba(118, 107, 106, 1); --color-bytebot-bronze-dark-10: rgba(132, 121, 119, 1); --color-bytebot-bronze-dark-11: rgba(187, 178, 176, 1); --color-bytebot-bronze-dark-12: rgba(239, 238, 237, 1); --color-bytebot-bronze-dark-a1: rgba(187, 62, 0, 0.0117647061124444); --color-bytebot-bronze-dark-a2: rgba(249, 203, 180, 0.04313725605607033); --color-bytebot-bronze-dark-a3: rgba(249, 214, 202, 0.08627451211214066); --color-bytebot-bronze-dark-a4: rgba(255, 221, 213, 0.11764705926179886); --color-bytebot-bronze-dark-a5: rgba(253, 220, 214, 0.15294118225574493); --color-bytebot-bronze-dark-a6: rgba(252, 222, 217, 0.1679999977350235); --color-bytebot-bronze-dark-a7: rgba(253, 225, 221, 0.2549019753932953); --color-bytebot-bronze-dark-a8: rgba(253, 228, 223, 0.364705890417099); --color-bytebot-bronze-dark-a9: rgba(253, 228, 225, 0.4274509847164154); --color-bytebot-bronze-dark-a10: rgba(253, 231, 227, 0.48627451062202454); --color-bytebot-bronze-dark-a11: rgba(254, 241, 238, 0.7176470756530762); --color-bytebot-bronze-dark-a12: rgba(255, 254, 253, 0.9333333373069763); /* Red light */ --color-bytebot-red-light-9: rgba(229, 72, 77, 1); --color-bytebot-red-light-1: rgba(255, 252, 252, 1); --color-bytebot-red-light-2: rgba(255, 247, 247, 1); --color-bytebot-red-light-3: rgba(254, 235, 236, 1); --color-bytebot-red-light-4: rgba(255, 219, 220, 1); --color-bytebot-red-light-5: rgba(255, 205, 206, 1); --color-bytebot-red-light-6: rgba(253, 189, 190, 1); --color-bytebot-red-light-7: rgba(244, 169, 170, 1); --color-bytebot-red-light-8: rgba(235, 142, 144, 1); --color-bytebot-red-light-10: rgba(220, 62, 66, 1); --color-bytebot-red-light-11: rgba(206, 44, 49, 1); --color-bytebot-red-light-12: rgba(100, 23, 35, 1); /* Red dark */ --color-bytebot-red-dark-1: rgba(23, 15, 14, 1); --color-bytebot-red-dark-2: rgba(32, 19, 18, 1); --color-bytebot-red-dark-3: rgba(59, 18, 18, 1); --color-bytebot-red-dark-4: rgba(80, 15, 19, 1); --color-bytebot-red-dark-5: rgba(97, 23, 26, 1); --color-bytebot-red-dark-6: rgba(115, 36, 37, 1); --color-bytebot-red-dark-7: rgba(140, 52, 52, 1); --color-bytebot-red-dark-8: rgba(181, 69, 70, 1); --color-bytebot-red-dark-9: rgba(229, 72, 77, 1); --color-bytebot-red-dark-10: rgba(230, 86, 91, 1); --color-bytebot-red-dark-11: rgba(255, 143, 139, 1); --color-bytebot-red-dark-12: rgba(255, 210, 206, 1); /* Green */ --color-bytebot-green-3: rgba(232, 247, 228, 1); --color-bytebot-green-4: rgba(218, 242, 211, 1); --color-bytebot-green-5: rgba(200, 234, 190, 1); --color-bytebot-green-6: rgba(178, 223, 165, 1); --color-bytebot-green-7: rgba(148, 208, 130, 1); --color-bytebot-green-8: rgba(103, 188, 77, 1); --color-bytebot-green-9: rgba(77, 175, 41, 1); --color-bytebot-green-10: rgba(68, 162, 32, 1); --color-bytebot-green-11: rgba(43, 128, 0, 1); --color-bytebot-green-12: rgba(33, 61, 24, 1); --color-bytebot-green-2: rgba(245, 251, 244, 1); --color-bytebot-green-1: rgba(251, 254, 250, 1); --color-bytebot-green-a1: rgba(51, 204, 0, 0.019600000232458115); --color-bytebot-green-a2: rgba(24, 163, 0, 0.04309999942779541); --color-bytebot-green-a3: rgba(38, 180, 0, 0.10589999705553055); --color-bytebot-green-a4: rgba(41, 180, 0, 0.17249999940395355); --color-bytebot-green-a5: rgba(40, 173, 0, 0.2549000084400177); --color-bytebot-green-a6: rgba(37, 165, 1, 0.3528999984264374); --color-bytebot-green-a7: rgba(37, 160, 0, 0.490200012922287); --color-bytebot-green-a8: rgba(37, 159, 0, 0.6980000138282776); --color-bytebot-green-a9: rgba(43, 160, 0, 0.8392000198364258); --color-bytebot-green-a10: rgba(41, 149, 0, 0.8744999766349792); --color-bytebot-green-a11: rgba(43, 128, 0, 1); --color-bytebot-green-a12: rgba(10, 41, 0, 0.9059000015258789); /* letterSpacing */ --letter-spacing-wide: 0.02em; --letter-spacing-normal: 0em; --letter-spacing-narrow: -0.009999999776482582em; --letter-spacing-narrow: -0.01em; /* Shadow */ --shadow-bytebot: 0px 0px 0px 1.5px #FFF inset; } :root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.147 0.004 49.25); --card: oklch(1 0 0); --card-foreground: oklch(0.147 0.004 49.25); --popover: oklch(1 0 0); --popover-foreground: oklch(0.147 0.004 49.25); --primary: oklch(0.216 0.006 56.043); --primary-foreground: oklch(0.985 0.001 106.423); --secondary: oklch(0.97 0.001 106.424); --secondary-foreground: oklch(0.216 0.006 56.043); --muted: oklch(0.97 0.001 106.424); --muted-foreground: oklch(0.553 0.013 58.071); --accent: oklch(0.97 0.001 106.424); --accent-foreground: oklch(0.216 0.006 56.043); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.923 0.003 48.717); --input: oklch(0.923 0.003 48.717); --ring: oklch(0.709 0.01 56.259); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0.001 106.423); --sidebar-foreground: oklch(0.147 0.004 49.25); --sidebar-primary: oklch(0.216 0.006 56.043); --sidebar-primary-foreground: oklch(0.985 0.001 106.423); --sidebar-accent: oklch(0.97 0.001 106.424); --sidebar-accent-foreground: oklch(0.216 0.006 56.043); --sidebar-border: oklch(0.923 0.003 48.717); --sidebar-ring: oklch(0.709 0.01 56.259); } /* .dark { --background: oklch(0.147 0.004 49.25); --foreground: oklch(0.985 0.001 106.423); --card: oklch(0.216 0.006 56.043); --card-foreground: oklch(0.985 0.001 106.423); --popover: oklch(0.216 0.006 56.043); --popover-foreground: oklch(0.985 0.001 106.423); --primary: oklch(0.923 0.003 48.717); --primary-foreground: oklch(0.216 0.006 56.043); --secondary: oklch(0.268 0.007 34.298); --secondary-foreground: oklch(0.985 0.001 106.423); --muted: oklch(0.268 0.007 34.298); --muted-foreground: oklch(0.709 0.01 56.259); --accent: oklch(0.268 0.007 34.298); --accent-foreground: oklch(0.985 0.001 106.423); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.553 0.013 58.071); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.216 0.006 56.043); --sidebar-foreground: oklch(0.985 0.001 106.423); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0.001 106.423); --sidebar-accent: oklch(0.268 0.007 34.298); --sidebar-accent-foreground: oklch(0.985 0.001 106.423); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.553 0.013 58.071); } */ @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; } } .hide-scrollbar { scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE 10+ */ } .hide-scrollbar::-webkit-scrollbar { display: none; /* Chrome, Safari, Opera */ } ================================================ FILE: packages/bytebot-ui/src/app/layout.tsx ================================================ import type React from "react"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Bytebot", description: "Bytebot is the container for desktop agents.", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: packages/bytebot-ui/src/app/page.tsx ================================================ "use client"; import React, { useState, useEffect, useRef } from "react"; import Image from "next/image"; import { Header } from "@/components/layout/Header"; import { ChatInput } from "@/components/messages/ChatInput"; import { useRouter } from "next/navigation"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { startTask } from "@/utils/taskUtils"; import { Model } from "@/types"; import { TaskList } from "@/components/tasks/TaskList"; interface StockPhotoProps { src: string; alt?: string; } const StockPhoto: React.FC = ({ src, alt = "Decorative image", }) => { return (
{alt}
); }; interface FileWithBase64 { name: string; base64: string; type: string; size: number; } export default function Home() { const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(null); const [uploadedFiles, setUploadedFiles] = useState([]); const router = useRouter(); const [activePopoverIndex, setActivePopoverIndex] = useState( null, ); const popoverRef = useRef(null); const buttonsRef = useRef(null); useEffect(() => { fetch("/api/tasks/models") .then((res) => res.json()) .then((data) => { setModels(data); if (data.length > 0) setSelectedModel(data[0]); }) .catch((err) => console.error("Failed to load models", err)); }, []); // Close popover when clicking outside or pressing ESC useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( popoverRef.current && !popoverRef.current.contains(event.target as Node) && buttonsRef.current && !buttonsRef.current.contains(event.target as Node) ) { setActivePopoverIndex(null); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setActivePopoverIndex(null); } }; if (activePopoverIndex !== null) { document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleKeyDown); } return () => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleKeyDown); }; }, [activePopoverIndex]); const handleSend = async () => { if (!input.trim()) return; setIsLoading(true); try { if (!selectedModel) throw new Error("No model selected"); // Send request to start a new task const taskData: { description: string; model: Model; files?: FileWithBase64[]; } = { description: input, model: selectedModel, }; // Include files if any are uploaded if (uploadedFiles.length > 0) { taskData.files = uploadedFiles; } const task = await startTask(taskData); if (task && task.id) { // Redirect to the task page router.push(`/tasks/${task.id}`); } else { // Handle error console.error("Failed to create task"); } } catch (error) { console.error("Error sending message:", error); } finally { setIsLoading(false); } }; const handleFileUpload = (files: FileWithBase64[]) => { setUploadedFiles(files); }; return (
{/* Desktop grid layout (50/50 split) - only visible on large screens */}
{/* Main content area */}

What can I help you get done?

{/* Stock photo area - centered in its grid cell */}
{/* Mobile layout - only visible on small/medium screens */}

What can I help you get done?

); } ================================================ FILE: packages/bytebot-ui/src/app/tasks/[id]/page.tsx ================================================ "use client"; import React, { useEffect, useRef } from "react"; import { Header } from "@/components/layout/Header"; import { ChatContainer } from "@/components/messages/ChatContainer"; import { DesktopContainer } from "@/components/ui/desktop-container"; import { useChatSession } from "@/hooks/useChatSession"; import { useScrollScreenshot } from "@/hooks/useScrollScreenshot"; import { useParams, useRouter } from "next/navigation"; import { Role, TaskStatus } from "@/types"; import { HugeiconsIcon } from "@hugeicons/react"; import { MoreVerticalCircle01Icon, WavingHand01Icon, } from "@hugeicons/core-free-icons"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { VirtualDesktopStatus } from "@/components/VirtualDesktopStatusHeader"; export default function TaskPage() { const params = useParams(); const router = useRouter(); const taskId = params.id as string; const chatContainerRef = useRef(null); const { messages, groupedMessages, taskStatus, control, input, setInput, isLoading, isLoadingSession, isLoadingMoreMessages, hasMoreMessages, loadMoreMessages, handleAddMessage, handleTakeOverTask, handleResumeTask, handleCancelTask, currentTaskId, } = useChatSession({ initialTaskId: taskId }); // Determine if task is inactive (show screenshot) or active (show VNC) function isTaskInactive(): boolean { return ( taskStatus === TaskStatus.COMPLETED || taskStatus === TaskStatus.FAILED || taskStatus === TaskStatus.CANCELLED ); } // Determine if user can take control function canTakeOver(): boolean { return control === Role.ASSISTANT && taskStatus === TaskStatus.RUNNING; } // Determine if user has control or is in takeover mode function hasUserControl(): boolean { return ( control === Role.USER && (taskStatus === TaskStatus.RUNNING || taskStatus === TaskStatus.NEEDS_HELP) ); } // Determine if task can be cancelled function canCancel(): boolean { return ( taskStatus === TaskStatus.RUNNING || taskStatus === TaskStatus.NEEDS_HELP ); } // Determine VNC mode - interactive when user has control, view-only otherwise function vncViewOnly(): boolean { return !hasUserControl(); } // Use scroll screenshot hook for inactive tasks const { currentScreenshot } = useScrollScreenshot({ messages, scrollContainerRef: chatContainerRef, }); // For inactive tasks, auto-load all messages for proper screenshot navigation useEffect(() => { if (isTaskInactive() && hasMoreMessages && !isLoadingMoreMessages) { loadMoreMessages(); } }, [ isTaskInactive(), hasMoreMessages, isLoadingMoreMessages, loadMoreMessages, ]); // Map each message ID to its flat index for screenshot scroll logic const messageIdToIndex = React.useMemo(() => { const map: Record = {}; messages.forEach((msg, idx) => { map[msg.id] = idx; }); return map; }, [messages]); // Redirect if task ID doesn't match current task useEffect(() => { if (currentTaskId && currentTaskId !== taskId) { router.push(`/tasks/${currentTaskId}`); } }, [currentTaskId, taskId, router]); return (
{/* Main container */}
{ if ( taskStatus === TaskStatus.RUNNING && control === Role.USER ) return "user_control"; if (taskStatus === TaskStatus.RUNNING) return "running"; if (taskStatus === TaskStatus.NEEDS_HELP) return "needs_attention"; if (taskStatus === TaskStatus.FAILED) return "failed"; if (taskStatus === TaskStatus.CANCELLED) return "canceled"; if (taskStatus === TaskStatus.COMPLETED) return "completed"; // You may want to add a scheduled state if you have that info return "pending"; })() as VirtualDesktopStatus } > {canTakeOver() && ( )} {hasUserControl() && ( )} {canCancel() && ( Cancel )}
{/* Chat Area */}
{/* Messages scrollable area */}
); } ================================================ FILE: packages/bytebot-ui/src/app/tasks/page.tsx ================================================ "use client"; import React, { useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Header } from "@/components/layout/Header"; import { TaskItem } from "@/components/tasks/TaskItem"; import { TaskTabs, TabKey, TAB_CONFIGS } from "@/components/tasks/TaskTabs"; import { Pagination } from "@/components/ui/pagination"; import { fetchTasks, fetchTaskCounts } from "@/utils/taskUtils"; import { Task } from "@/types"; import { Button } from "@/components/ui/button"; import Link from "next/link"; import { Suspense } from "react"; function TasksPageContent() { const router = useRouter(); const searchParams = useSearchParams(); const [tasks, setTasks] = useState([]); const [isLoading, setIsLoading] = useState(true); // Initialize activeTab from URL params const getInitialTab = (): TabKey => { const tabParam = searchParams.get("tab"); if (tabParam && Object.keys(TAB_CONFIGS).includes(tabParam)) { return tabParam as TabKey; } return "ALL"; }; const [activeTab, setActiveTab] = useState(getInitialTab); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [total, setTotal] = useState(0); const [taskCounts, setTaskCounts] = useState>({ ALL: 0, ACTIVE: 0, COMPLETED: 0, CANCELLED_FAILED: 0, }); const PAGE_SIZE = 10; useEffect(() => { const loadTasks = async () => { setIsLoading(true); try { const statuses = activeTab === "ALL" ? undefined : TAB_CONFIGS[activeTab].statuses; const result = await fetchTasks({ page: currentPage, limit: PAGE_SIZE, statuses, }); setTasks(result.tasks); setTotal(result.total); setTotalPages(result.totalPages); } catch (error) { console.error("Failed to load tasks:", error); } finally { setIsLoading(false); } }; loadTasks(); }, [currentPage, activeTab]); useEffect(() => { const loadTaskCounts = async () => { try { const counts = await fetchTaskCounts(); setTaskCounts(counts); } catch (error) { console.error("Failed to load task counts:", error); } }; loadTaskCounts(); }, []); // Sync activeTab with URL params when they change useEffect(() => { const tabParam = searchParams.get("tab"); const newTab: TabKey = tabParam && Object.keys(TAB_CONFIGS).includes(tabParam) ? (tabParam as TabKey) : "ALL"; if (newTab !== activeTab) { setActiveTab(newTab); setCurrentPage(1); } }, [searchParams, activeTab]); const handleTabChange = (tab: TabKey) => { setActiveTab(tab); setCurrentPage(1); // Update URL with the new tab const newSearchParams = new URLSearchParams(searchParams); if (tab === "ALL") { newSearchParams.delete("tab"); } else { newSearchParams.set("tab", tab); } const newUrl = `/tasks${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`; router.push(newUrl, { scroll: false }); }; const handlePageChange = (page: number) => { setCurrentPage(page); }; return (

Tasks

{!isLoading && ( )} {isLoading ? (

Loading tasks...

) : tasks.length === 0 ? (

No tasks yet

Get started by creating a first task

) : ( <>
{tasks.map((task) => ( ))}
{totalPages > 1 && ( )} )}
); } function TasksPageFallback() { return (

Loading tasks...

); } export default function TasksPage() { return ( }> ); } ================================================ FILE: packages/bytebot-ui/src/components/VirtualDesktopStatusHeader.tsx ================================================ import React from "react"; import Image from "next/image"; import { cn } from "@/lib/utils"; // Status types based on the image export type VirtualDesktopStatus = | "running" | "needs_attention" | "failed" | "canceled" | "pending" | "user_control" | "completed" | "live_view"; interface StatusConfig { dot: React.ReactNode; text: string; gradient: string; subtext: string; } const statusConfig: Record = { live_view: { dot: ( Live view status ), text: "Live Desktop View", gradient: "from-gray-700 to-gray-900", subtext: "", }, running: { dot: ( Running status ), text: "Running", gradient: "from-green-700 to-green-900", subtext: "Task in progress", }, needs_attention: { dot: ( Needs attention status ), text: "Needs Attention", gradient: "from-yellow-600 to-orange-700", subtext: "Task needs attention", }, failed: { dot: ( Failed status ), text: "Failed", gradient: "from-red-700 to-red-900", subtext: "Task failed", }, canceled: { dot: ( Canceled status ), text: "Canceled", gradient: "from-gray-400 to-gray-600", subtext: "Task canceled", }, pending: { dot: ( Pending status ), text: "Pending", gradient: "from-gray-400 to-gray-600", subtext: "Task pending", }, user_control: { dot: ( User control status ), text: "Running", gradient: "from-pink-500 to-fuchsia-700", subtext: "You took control", }, completed: { dot: ( Completed status ), text: "Completed", gradient: "from-green-700 to-green-900", subtext: "Task completed", }, }; export interface VirtualDesktopStatusHeaderProps { status: VirtualDesktopStatus; subtext?: string; // allow override className?: string; } export const VirtualDesktopStatusHeader: React.FC< VirtualDesktopStatusHeaderProps > = ({ status, subtext, className }) => { const config = statusConfig[status]; return (
{config.dot}
{config.text} {config.subtext && ( {subtext || config.subtext} )}
); }; ================================================ FILE: packages/bytebot-ui/src/components/layout/Header.tsx ================================================ import React, { useState, useEffect } from "react"; import Link from "next/link"; import Image from "next/image"; import { useTheme } from "next-themes"; import { HugeiconsIcon } from "@hugeicons/react"; import { DocumentCodeIcon, TaskDaily01Icon, Home01Icon, ComputerIcon, } from "@hugeicons/core-free-icons"; import { usePathname } from "next/navigation"; export function Header() { const { resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); const pathname = usePathname(); // After mounting, we can safely show the theme-dependent content useEffect(() => { setMounted(true); }, []); // Function to determine if a link is active const isActive = (path: string) => { if (path === "/") { return pathname === "/"; } return pathname?.startsWith(path); }; // Get classes for navigation links based on active state const getLinkClasses = (path: string) => { const baseClasses = "flex items-center gap-1.5 transition-colors px-3 py-1.5 rounded-lg"; const activeClasses = "bg-bytebot-bronze-light-a3 text-bytebot-bronze-light-12"; const inactiveClasses = "text-bytebot-bronze-dark-9 hover:bg-bytebot-bronze-light-a1 hover:text-bytebot-bronze-light-12"; return `${baseClasses} ${isActive(path) ? activeClasses : inactiveClasses}`; }; return (
{/* Logo without link */}
{mounted ? ( Bytebot Logo ) : (
)}
Home Tasks Desktop Docs
); } ================================================ FILE: packages/bytebot-ui/src/components/messages/AssistantMessage.tsx ================================================ import React from "react"; import { GroupedMessages, TaskStatus } from "@/types"; import { MessageAvatar } from "./MessageAvatar"; import { MessageContent } from "./content/MessageContent"; import { isToolResultContentBlock, isImageContentBlock } from "@bytebot/shared"; import Image from "next/image"; import { cn } from "@/lib/utils"; interface AssistantMessageProps { group: GroupedMessages; taskStatus: TaskStatus; messageIdToIndex: Record; } export function AssistantMessage({ group, taskStatus, messageIdToIndex, }: AssistantMessageProps) { return (
{group.take_over ? (
User control status

You took control

{group.messages.map((message) => (
{/* Render hidden divs for each screenshot block */} {message.content.map((block, blockIndex) => { if ( isToolResultContentBlock(block) && block.content && block.content.length > 0 ) { // Check ALL content items in the tool result, not just the first one const markers: React.ReactNode[] = []; block.content.forEach((contentItem, contentIndex) => { if (isImageContentBlock(contentItem)) { markers.push(
); } }); return markers; } return null; })}
))}
) : (
{group.messages.map((message) => (
{/* Render hidden divs for each screenshot block */} {message.content.map((block, blockIndex) => { if ( isToolResultContentBlock(block) && !block.is_error && block.content && block.content.length > 0 ) { // Check ALL content items in the tool result, not just the first one const markers: React.ReactNode[] = []; block.content.forEach((contentItem, contentIndex) => { if (isImageContentBlock(contentItem)) { markers.push(
); } }); return markers; } return null; })}
))}
)}
); } ================================================ FILE: packages/bytebot-ui/src/components/messages/ChatContainer.tsx ================================================ import React, { useRef, useEffect, useCallback, Fragment } from "react"; import { Role, TaskStatus, GroupedMessages } from "@/types"; import { MessageGroup } from "./MessageGroup"; import { TextShimmer } from "../ui/text-shimmer"; import { MessageAvatar } from "./MessageAvatar"; import { Loader } from "../ui/loader"; import { ChatInput } from "./ChatInput"; interface ChatContainerProps { scrollRef?: React.RefObject; messageIdToIndex: Record; taskId: string; input: string; setInput: (value: string) => void; isLoading: boolean; handleAddMessage: () => Promise; groupedMessages: GroupedMessages[]; taskStatus: TaskStatus; control: Role; isLoadingSession: boolean; isLoadingMoreMessages: boolean; hasMoreMessages: boolean; loadMoreMessages: () => Promise; } export function ChatContainer({ scrollRef, messageIdToIndex, input, setInput, isLoading, handleAddMessage, groupedMessages, taskStatus, control, isLoadingSession, isLoadingMoreMessages, hasMoreMessages, loadMoreMessages, }: ChatContainerProps) { const messagesEndRef = useRef(null); // Infinite scroll handler const handleScroll = useCallback(() => { if (!scrollRef?.current || !loadMoreMessages) { return; } const container = scrollRef.current; // Check if user scrolled to the bottom (within 20px - much more sensitive) const { scrollTop, scrollHeight, clientHeight } = container; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; if (distanceFromBottom <= 20 && hasMoreMessages && !isLoadingMoreMessages) { loadMoreMessages(); } }, [scrollRef, loadMoreMessages, hasMoreMessages, isLoadingMoreMessages]); // Add scroll event listener useEffect(() => { const container = scrollRef?.current; if (container) { container.addEventListener("scroll", handleScroll); return () => container.removeEventListener("scroll", handleScroll); } }, [handleScroll, scrollRef]); // This effect runs whenever the grouped messages array changes useEffect(() => { if ( taskStatus === TaskStatus.RUNNING || taskStatus === TaskStatus.NEEDS_HELP ) { scrollToBottom(); } }, [taskStatus, groupedMessages]); // Function to scroll to the bottom of the messages const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; return (
{isLoadingSession ? (
) : groupedMessages.length > 0 ? ( <> {/* Content area - scrolling handled by parent */}
{groupedMessages.map((group, groupIndex) => ( ))} {taskStatus === TaskStatus.RUNNING && control === Role.ASSISTANT && (
Bytebot is working...
)} {/* Loading indicator for infinite scroll at bottom */} {isLoadingMoreMessages && (
)} {/* This empty div is the target for scrolling */}
{/* Fixed chat input at bottom */} {[TaskStatus.RUNNING, TaskStatus.NEEDS_HELP].includes(taskStatus) && (
)} ) : (

No messages yet...

)}
); } ================================================ FILE: packages/bytebot-ui/src/components/messages/ChatInput.tsx ================================================ import React, { useRef, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowRight02Icon, Attachment01Icon, Cancel01Icon } from "@hugeicons/core-free-icons"; import { cn } from "@/lib/utils"; interface FileWithBase64 { name: string; base64: string; type: string; size: number; } interface ChatInputProps { input: string; isLoading: boolean; onInputChange: (value: string) => void; onSend: () => void; onFileUpload?: (files: FileWithBase64[]) => void; minLines?: number; placeholder?: string; } export function ChatInput({ input, isLoading, onInputChange, onSend, onFileUpload, minLines = 1, placeholder = "Give Bytebot a task to work on...", }: ChatInputProps) { const textareaRef = useRef(null); const fileInputRef = useRef(null); const [selectedFiles, setSelectedFiles] = useState([]); const [errorMessage, setErrorMessage] = useState(""); const MAX_FILES = 5; const MAX_FILE_SIZE = 30 * 1024 * 1024; // 30MB per file in bytes const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSend(); }; const handleFileSelect = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; setErrorMessage(""); // Check max files limit if (selectedFiles.length + files.length > MAX_FILES) { setErrorMessage(`Maximum ${MAX_FILES} files allowed`); e.target.value = ''; return; } // Check individual file sizes const oversizedFiles: string[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.size > MAX_FILE_SIZE) { oversizedFiles.push(`${file.name} (${formatFileSize(file.size)})`); } } if (oversizedFiles.length > 0) { setErrorMessage(`File(s) exceed 30MB limit: ${oversizedFiles.join(', ')}`); e.target.value = ''; return; } const newFiles: FileWithBase64[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const base64 = await convertToBase64(file); newFiles.push({ name: file.name, base64: base64, type: file.type, size: file.size, }); } const updatedFiles = [...selectedFiles, ...newFiles]; setSelectedFiles(updatedFiles); if (onFileUpload) { onFileUpload(updatedFiles); } // Reset the input e.target.value = ''; }; const convertToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = (error) => reject(error); }); }; const removeFile = (index: number) => { const updatedFiles = selectedFiles.filter((_, i) => i !== index); setSelectedFiles(updatedFiles); setErrorMessage(""); if (onFileUpload) { onFileUpload(updatedFiles); } }; const triggerFileInput = () => { fileInputRef.current?.click(); }; const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // Auto-resize textarea based on content useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; // Reset height to auto to get the correct scrollHeight textarea.style.height = "auto"; // Calculate minimum height based on minLines const lineHeight = 24; // approximate line height in pixels const minHeight = lineHeight * minLines + 12; // Set height to scrollHeight or minHeight, whichever is larger const newHeight = Math.max(textarea.scrollHeight, minHeight); textarea.style.height = `${newHeight}px`; }, [input, minLines]); // Determine button position based on minLines const buttonPositionClass = minLines > 1 ? "bottom-1.5" : "top-1/2 -translate-y-1/2"; return (
{errorMessage && (
{errorMessage}
)} {selectedFiles.length > 0 && (
{selectedFiles.length} / {MAX_FILES} files Max 30MB per file
{selectedFiles.map((file, index) => (
{file.name}
))}
)}