Compare commits

..

117 Commits

Author SHA1 Message Date
Robert Brennan d34a9db04f Run docker publish on tags (#1162)
* run ghcr on tags

* Update .github/workflows/ghcr.yml

Co-authored-by: RaGe <foragerr@users.noreply.github.com>

---------

Co-authored-by: RaGe <foragerr@users.noreply.github.com>
2024-04-16 13:27:31 -04:00
Alex Bäuerle 7710112ae2 fix: fix delete messages endpoint url (#1140)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-16 17:08:18 +00:00
Alex Bäuerle 6b007c163b fix: make typing chat bubble adhere to max width (#1142) 2024-04-16 10:02:04 -07:00
Engel Nyst 1115b60a74 Logging additions and fixes (#1139)
* Refactor print_to_color into a color formatter

misc fixes

catch ValueErrors and others from Router initialization

add default methods

* Tweak console log formatting, clean up after rebasing exceptions out

* Fix prompts/responses

* clean up

* keep regular colors when no msg_type

* fix filename

* handle file log first

* happy mypy

* ok, mypy

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-16 12:55:22 -04:00
sp.wack 5881857d5c test(frontend): add unit tests (#1076)
* test(frontend): add unit tests for getCachedConfig

* test(frontend): add unit tests for getCachedConfig

* add unit test for the useTypingEffect hook

* add unit test for the useInputComposition hook

* create unit test for auth service

* remove outdated and failing component test

* create unit test for session service

* break down saveSettings into smaller functions for testability and create unit test for new mergeAndUpdateSettings

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-16 12:49:56 -04:00
Alex Bäuerle 4b4bc15fdb fix: fix multiple frontend warnings (#1143) 2024-04-16 10:06:31 -04:00
RaGe 45c6f639f4 (fix) OpenDevin works on OpenDevin issues (#1149)
* Use SANDBOX_TYPE=exec, use docker image

* Use updated image
2024-04-16 09:24:31 -04:00
Boxuan Li 90ca29414c Remove custom signal handlers (#1153) 2024-04-16 09:20:13 -04:00
Xia Zhenhua 8e4c4c9946 fix #1028, /select-file api call on deleted file in Code Editor caused Error (#1158)
* fix: /select-file on deleted file exception, detail: https://github.com/OpenDevin/OpenDevin/issues/1028

* fix: lint.

* fix: lint.

---------

Co-authored-by: aaren.xzh <aaren.xzh@antfin.com>
2024-04-16 09:19:10 -04:00
மனோஜ்குமார் பழனிச்சாமி 88e31f91d8 corrected port (#1159) 2024-04-16 09:18:12 -04:00
Akki 7e825b571f fix(): build out opendevin modal component (#1141) 2024-04-16 08:43:04 -04:00
Robert Brennan 516c9bf1e0 Revamp docker build process (#1121)
* refactor docker building

* change to buildx

* disable branch filter

* disable tags

* matrix for building

* fix branch filter

* rename workflow

* sanitize ref name

* fix sanitization

* fix source command

* fix source command

* add push arg

* enable for all branches

* logs

* empty commit

* try freeing disk space

* try disk clean again

* try alpine

* Update ghcr.yml

* Update ghcr.yml

* move checkout

* ignore .git

* add disk space debug

* add df h to build script

* remove pull

* try another failure bypass

* remove maximize build space step

* remove df -h debug

* add no-root

* multi-stage python build

* add ssh

* update readme

* remove references to config.toml
2024-04-15 19:10:38 -04:00
Alex Bäuerle 71edee17be build: fix workspace variable name in dev setup (#1138) 2024-04-15 16:33:49 -04:00
RaGe de672029ef Add build-frontend to build (#1137) 2024-04-15 13:00:25 -07:00
808vita f0559892ab lint-frontend Files.tsx (#1089)
for lint-frontend exhaustive deps

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-15 11:52:30 -07:00
Edwards.Arno 2491a3524e feat: add question.md template for community Q&A (#1115)
* feat: add `question.md` template for community Q&A

* Update .github/ISSUE_TEMPLATE/question.md

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-15 14:14:37 +00:00
மனோஜ்குமார் பழனிச்சாமி 0616fe3f8d Added Retry for LLM calls (#1092)
* added retry

* filtered API errors

* fixed decorator

* used litellm retries

* added custom backoff too

* Apply suggestions from code review

Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>

* added custom backoff too

* retried only if certain Exceptions

---------

Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-15 14:47:38 +02:00
Robert Brennan 474aafbc79 Fix frontend build (#1118) 2024-04-15 14:36:18 +02:00
Robert Brennan 342302ceef Add Docker DOOD setup (#1023)
* simplified get

* resolved merge conflicts

* removed default param for get

* add dood setup

* add readme

* better build process

* multi-stage build

* revert makefile

* rm entrypoint.sh

* adjust ssh box for docker

* update readme

* update readme

* fix hostname

* change workspace setting

* add workspace_mount_base

* fixes for workspace dir

* clean up frontend

* refactor dockerfile

* try download.py

* change docker order a bit

* remove workspace_dir from frontend settings

* fix merge issues

* Update opendevin/config.py

* remove relpath logic from server

* rename workspace_mount_base to workspace_base

* remove workspace dir plumbing for now

* delint

* delint

* move workspace base dir

* remove refs to workspace_dir

* factor out constant

* fix local directory usage

* dont require dir

* fix docs

* fix arg parsing for task

* implement WORKSPACE_MOUNT_PATH

* fix workspace dir

* fix ports

* fix merge issues

* add makefile

* revert settingsService

* fix string

* Add address

* Update Dockerfile

* Update local_box.py

* fix lint

* move to port 3000

---------

Co-authored-by: மனோஜ்குமார் பழனிச்சாமி <smartmanoj42857@gmail.com>
Co-authored-by: enyst <engel.nyst@gmail.com>
2024-04-15 14:19:02 +02:00
Xia Zhenhua 8450b47609 fix: action.run executed twice if action is not awaitable (#1021) 2024-04-15 12:12:07 +00:00
Robert Brennan 34ecfe3c75 Update dogfood.yml (#1107) 2024-04-15 10:02:31 +02:00
மனோஜ்குமார் பழனிச்சாமி f0cd5a37e8 Update bug_report.md (#1111) 2024-04-15 10:02:01 +02:00
Tunay Engin 98d019b825 Adding: Turkish language (#1102)
* Adding: Turkish language

* Add space

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
2024-04-15 03:07:59 +00:00
Sujay Kapadnis d2f7056c06 refactor: Added docstrings to BackgroundCommand(#1083) (#1093)
* refactor: Added docstrings to BackgroundCommand(#1083)

* refactor: Added Docstring with example for BackgroundCommand

* fixed typo

Co-authored-by: Graham Neubig <neubig@gmail.com>

---------

Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-04-14 22:13:33 +00:00
Boxuan Li 652507f430 Fix pre-commit and linter versions to avoid surprise (#1100)
* Fix pre-commit and linter versions to avoid surprise

To avoid surprising results on GitHub Actions, e.g. a new release of pre-commit starts to
reject all PRs, fix it to the latest version, 3.7.0. This PR also fixes ruff and mypy
versions in pyproject.toml since we very likely don't really need latest upgrades from
linters, and upgrades can always bring surprise.

* pre-commit-config: Use v0.3.7 for Ruff as in pyproject.toml
2024-04-14 21:33:58 +02:00
மனோஜ்குமார் பழனிச்சாமி 033352e340 added to sudo group (#1091) 2024-04-14 15:32:36 +02:00
Z 6b9316f722 doc: Add supplementary notes for WSL2 users to Local LLM Guide (#1031)
* Add supplementary notes for WSL2 users

* Add supplementary notes for WSL2 users

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-14 13:22:58 +00:00
Boxuan Li 53f95056de Revamp Exception handling (#1080)
* Revamp exception handling

* Agent controller: sleep 3 seconds if APIConnection error

* Fix AuthenticationError capture

* Revert unrelated style fixes

* Add type enforcement for action_from_dict call
2024-04-14 06:51:17 +02:00
Boxuan Li dd32fa6f4a Unify linter behaviour across CI and pre-commit-hook (#1071)
* CI: Add autopep8 linter

Currently, we have autopep8 as part of pre-commit-hook. To ensure
consistent behaviour, we should have it in CI as well.

Moreover, pre-commit-hook contains a double-quote-string-fixer hook
which changes all double quotes to single quotes, but I do observe
some PRs with massive changes that do the opposite way. I suspect
that these authors 1) disable or circumvent the pre-commit-hook,
and 2) have other linters such as black in their IDE, which
automatically change all single quotes to double quotes. This
has caused a lot of unnecessary diff, made review really hard,
and led to a lot of conflicts.

* Use -diff for autopep8

* autopep8: Freeze version in CI

* Ultimate fix

* Remove pep8 long line disable workaround

* Fix lint.yml

* Fix all files under opendevin and agenthub
2024-04-14 00:19:56 -04:00
Jim Su d4ce4ea541 Throw error if an illegal sandbox type is used (#1087) 2024-04-13 23:59:31 -04:00
Robert Brennan 066680f8ef Auto-close stale issues and PRs (#1032)
* stale issues

* Update .github/workflows/stale.yml

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>

* Update .github/workflows/stale.yml

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>

* Update .github/workflows/stale.yml

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>

* Update .github/workflows/stale.yml

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>

---------

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-04-14 02:39:46 +00:00
RaGe d27895d018 Add new sandbox type - local (#1029) 2024-04-13 20:47:11 -04:00
Akki 8b9f13b1ed fix(editor): ui enhancements and code refactor (#1069) 2024-04-13 08:52:26 -07:00
Leo 2326312d3a fix: print the wrong ssh port number (#1054) 2024-04-12 22:18:34 -04:00
Boxuan Li e0c7492609 Traffic Control: Add new config MAX_CHARS (#1015)
* Add new config MAX_CHARS

* Fix mypy linting issues
2024-04-12 19:01:52 +00:00
namtacs 5d5106c510 Response recognition for weak llms (#523)
* Tweak for weak llms

* Update to the latest commits

* Update to the latest commits

* Fix lint errors

* Remove merge artifact

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
2024-04-12 09:20:47 -04:00
மனோஜ்குமார் பழனிச்சாமி 70534f203e simplified get (#962)
* simplified get

* resolved merge conflicts

* removed default param for get

* Update opendevin/config.py

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-12 09:18:41 -04:00
Leo 494a1b6872 Feat add agent manager (#904)
* feat: add agent manager to manage all agents;

* extract the host ssh port to prevent conflict.

* clean all containers with prefix is sandbox-

* merge from upstream/main

* merge from upstream/main

* Update frontend/src/state/settingsSlice.ts

* Update opendevin/sandbox/ssh_box.py

* Update opendevin/sandbox/exec_box.py

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-12 07:53:47 -04:00
Engel Nyst ded0a762aa Formatting AZURE_LLM_GUIDE (#1046) 2024-04-12 11:28:42 +02:00
Engel Nyst 32ba0e7f7e Add Azure configuration doc (#1035)
* Add Azure configuration doc

* Add link to Azure doc.
2024-04-12 04:59:51 -04:00
PierrunoYT 7b526b3620 Add Italian, Spanish and Português (#1017)
* Update index.ts

Add Italian, Spanish and Português

* Update translation.json

Add Italian. Spanish and Português

* Remove unnecessary i18n initialization arguments

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
2024-04-12 04:57:12 -04:00
Alex Bäuerle 224ee7d1f8 fix: fix some of the styling to more closely match figma (#927)
* fix: fix some of the styling to more closely match figma

* overflow
2024-04-12 04:45:03 -04:00
Redrum624 9fd95cc35f Fix: local files in the browser (at least for Windows) (#839)
* Fix: local files in the browser (at least for Windows)

The Browser works for screenshoting websites, but when the LLM tries to display a local file that he created, it can't access the full directory on Windows.

* Update browse.py

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-11 22:12:23 -04:00
hugehope 9cd4ad3298 chore: fix some typos in comments (#1013)
Signed-off-by: hugehope <cmm7@sina.cn>
2024-04-11 23:21:46 +02:00
808vita fb54c36c90 lint-frontend settingsSlice.ts (#1022)
* lint-frontend settingsSlice.ts

for  lint-frontend
Unexpected console statement

* Update settingsSlice.ts
2024-04-11 19:25:04 +02:00
PierrunoYT 51b3ae56c7 Add German Translation (#851)
* Update index.ts

* Update translation.json

* Update index.ts

* Remove comment and ident JSON

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
2024-04-10 22:59:46 -04:00
Engel Nyst b8202e804f Make sure Azure info is read from toml (#1005) 2024-04-10 19:28:51 -04:00
Robert Brennan 7f5d9c7d92 Update agent_controller.py (#1004) 2024-04-10 19:28:30 -04:00
Alex Bäuerle cd723abfdd fix: make chat colors match figma (#957) 2024-04-10 18:24:25 -04:00
Robert Brennan cc584445c6 implement exec box (#983)
* implement exec box

* fix autopep8 issue

* fix mypy issue

* fix some bugs

* set default to ssh

* rename sandbox impls

* empty commit

* fix imports

---------

Co-authored-by: Anas Dorbani <anasdorbani@gmail.com>
2024-04-10 17:39:33 -04:00
Robert Brennan 9846e24299 Fix logger import (#985)
* fix logger import
* fix mypy version
* make mypy happy (#994)

---------

Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2024-04-10 21:48:40 +02:00
Engel Nyst 973a42fd78 More json cleaning (#924)
* More json cleaning

* remove redundant check
2024-04-10 20:03:20 +02:00
Leo b2de79ae08 Feat add toast (#951)
* feat: add agent status bar.

* fix: ws will reconnect after failed in first time.

* update: delete useless codes.

* feat: add toast
2024-04-10 12:10:58 -04:00
Ikko Eltociear Ashimine 0b21d77880 Update architecture/README.md (#970)
generat -> generate
2024-04-10 12:10:25 -04:00
libowen2121 e256329e5e Update SWE-bench eval results (#978) 2024-04-10 21:09:49 +08:00
Engel Nyst 52b63908ca FE config (#960)
* Send FE its own version of config

* Don't read some settings back

* Update opendevin/config.py

Co-authored-by: Robert Brennan <accounts@rbren.io>

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-10 15:08:09 +02:00
Xingyao Wang e8ff184912 Update README.md 2024-04-10 14:57:52 +08:00
Alex Bäuerle 707ab7b3f8 feat: make panes resizable (#925) 2024-04-09 20:42:16 -04:00
Robert Brennan 78858c18b5 Update README.md (#956) 2024-04-09 15:37:33 -05:00
dred0n 92562a5152 remove json_dump and adjust handlers (#881)
Co-authored-by: senseable <dondre+witcebs@gmail.com>
2024-04-09 14:41:07 -04:00
Xiaochen Duan c1aab59c2e i18n : Add zh-TW 繁體中文 (#947)
* add zh-TW translation

* add zh-TW translation
2024-04-09 13:22:24 -05:00
Leo eb7b3484fd feat: add agent status bar. (#941)
* feat: add agent status bar.

* fix: ws will reconnect after failed in first time.

* update: delete useless codes.
2024-04-09 15:26:06 +00:00
Engel Nyst 8ab9c6fb86 Revert the use of Router, good ole completion works. (#910)
* Revert the use of Router, good ole completion works.

* Stopgap exception message

* Get the updated dependencies.
2024-04-08 22:21:30 -04:00
Alex Bäuerle 3c4b3eddc0 fix: text input can be edited even if not yet initialized (#928)
Clicking the send button is still only possible once initialized.
2024-04-09 00:01:49 +02:00
ITNerdAZ 01c4c4bee4 Add installation warning to readme (#883)
* Update README

* Update README.md

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-08 09:56:02 -05:00
Xingyao Wang 3c82ba7f34 support running sudo in a passwordless manner (#906) 2024-04-08 22:54:06 +08:00
Exlo 73fb4843a3 i18n : Add Norwegian (#876)
* Update index.ts

* Update translation.json

* Update index.ts
2024-04-08 10:36:04 -04:00
Akki 6f795f5e9c style(code editor): improved UI / UX for code editor (#826)
* style(): improved code edito ui / ux

* fix(): fix build issue and use cn fn

* theme variable updated to tailwind neutral gray

* fix(): fix conflicts

* fix lint errors

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
2024-04-08 10:32:45 -04:00
Xingyao Wang 6b7c5b09af fix(sandbox): Fix docker root login and set default to run-as-devin (#891)
* support execute as-root for sandbox

* set run as devin by default to true

* get network_mode = host back

* Update opendevin/sandbox/sandbox.py

Co-authored-by: Anas DORBANI <95044293+dorbanianas@users.noreply.github.com>

* print login info for debugging

* use ssh -v instead of ssh

* change port map to 2222 to circumvent the MacOS issue

* add warning message for port forwarding

---------

Co-authored-by: Anas DORBANI <95044293+dorbanianas@users.noreply.github.com>
2024-04-08 21:34:07 +08:00
Yufan Song 6e566dd21d fix: change default RUN_AS_DEVIN to true and change some network setting (#895)
* fix use devin

* fix port problem
2024-04-08 02:23:25 -07:00
Robert Brennan cc6626ff0d fix up json parsing (#875) 2024-04-08 14:39:36 +08:00
Xingyao Wang fab2259d3a fix(ghcr image) bump build tag minor version to fix docker build (#884) 2024-04-08 13:32:34 +08:00
Xingyao Wang 55760ec4dd feat(sandbox): Support sshd-based stateful docker session (#847)
* support sshd-based stateful docker session

* use .getLogger to avoid same logging message to get printed twice

* update poetry lock for dependency

* fix ruff

* bump docker image version with sshd

* set-up random user password and only allow localhost connection for sandbox

* fix poetry

* move apt install up
2024-04-08 12:59:18 +08:00
RaGe 6e3b554317 Create a CommandExecutor abstract class (#874)
* Create abstract CommandExecutor class

* Use CommandExecutor for Sandbox
2024-04-07 14:57:31 -05:00
Leo e52bf5ad7b Fix aligning settings between fe and be (#863)
* fix: aligning settings between FE and BE.

* apply black formatter and clean useless codes.
2024-04-07 14:56:14 -05:00
Robert Brennan e878b0c7ee Remove old references to pip (#871)
* Update README.md

* Update README.md
2024-04-07 13:21:07 -05:00
Engel Nyst 2f9bf606c7 Don't save backend file (#870)
* Don't save backend file

* Update Makefile

Co-authored-by: Anas DORBANI <95044293+dorbanianas@users.noreply.github.com>

---------

Co-authored-by: Anas DORBANI <95044293+dorbanianas@users.noreply.github.com>
2024-04-07 19:39:32 +02:00
Engel Nyst 04066ca42b fix default logging (#867) 2024-04-07 18:55:52 +02:00
iamiks a0c5c8efe9 i18n : Add Korean (#849) 2024-04-07 10:40:18 -05:00
808vita 6f346b3789 refactor & fix frontend: typing chat (#861) 2024-04-07 10:40:03 -05:00
Xingyao Wang e9121b78fe use .getLogger to avoid same logging message to get printed twice (#850) 2024-04-07 04:07:59 -04:00
Anas DORBANI d3770f1db6 add github action for project build on macos and linux (#838)
* update github action to build and run tests  on macos and linux

* fix docker installation

* Fix poetry installation on macos

* Fix docker installation

* Fix docker installation - start docker daemon

* Change docker installation macos

* Update docker buildx version

* new docker installation

* Add new start docker structure

* Add new start docker structure 2

* update github action to build and run tests  on macos and linux

* Update makefile to fix chroma-hnswlib issue with macos

* fix macos build

* Fix macos issue

* Fix macos

* Reformat Makefile

* updates
2024-04-07 03:54:52 -04:00
Engel Nyst 99a8dc4ff9 Fallback to less expensive model (#475) 2024-04-07 05:45:37 +02:00
Engel Nyst 4b4ce20f2d Add logging (#660)
* Add logging config for the app and for llm debug

* - switch to python, add special llm logger

- add logging to sandbox.py

- add session.py

- add a directory per session

- small additions for AgentController

* - add sys log, but try to exclude litellm; log llm responses as json

* Update opendevin/_logging.py

Co-authored-by: Anas DORBANI <95044293+dorbanianas@users.noreply.github.com>

* - use standard file naming
- quick pass through a few more files

* fix ruff

* clean up

* mypy types

* make mypy happy

---------

Co-authored-by: Anas DORBANI <95044293+dorbanianas@users.noreply.github.com>
2024-04-07 05:43:25 +02:00
Sri d87a7ddd83 moving type and eslint related packages to dev dependencies (#811)
Co-authored-by: msri23 <112919748+msri23@users.noreply.github.com>
2024-04-06 22:40:51 -04:00
Anas DORBANI 9c98b67002 Fix awk error for the WSL users (#837) 2024-04-07 00:56:15 +00:00
AbhisekOmkar c0dfc851b9 Add Discord and Slack Community Link to README (#835)
This pull request enhances the README.md file by adding a Discord community link to the OpenDevin project. The addition includes a Discord logo with a hyperlink to the project's Discord server invite, ensuring easy access for users interested in joining the community. This update maintains consistency with existing style elements, such as badges for other community platforms like Slack. By incorporating the Discord link, this pull request aims to promote community engagement and improve user accessibility within the project.
2024-04-06 13:30:58 -05:00
AbhisekOmkar 5ce3af6f8f Update README.md (#832) 2024-04-06 13:02:44 -05:00
RaGe 371f7a127d (feat) Use OpenDevin to address OpenDevin issues (#803)
* Initial attempt

* Add `poetry run` to command

* Add poetry dependency cache

* cache needs poetry to be installed first :/

* escape newlines in issue body

* Write task to file instead

* Add LLM_API_KEY

* Attempt PR generation

* Bring back agent operations

* set-output is deprecated, use GITHUB_ENV

* PR should target default branch
2024-04-06 13:49:21 -04:00
Anas DORBANI e54fe4491c Update README.md
Add poetry to requirements
2024-04-06 17:47:33 +00:00
Leo 3313e473ea style: Action and Observation use schema for unifying (#494)
* style: Action and Observation use schema for unifying

* merge from upstream/main

* merge from upstream/main
2024-04-06 13:46:07 -04:00
Xingyao Wang 229fa988c5 remove seed=42 to fix #813 (#830) 2024-04-06 13:04:17 -04:00
Junyang Lin 5dda0ddef6 Update README.md
change discord invite link
2024-04-07 00:55:35 +08:00
Aaron Jorgensen 23a7057be2 Update SettingModal.tsx (#792) 2024-04-06 12:41:48 -04:00
Graham Neubig f40fe6ac28 Remove pnpm (#823)
* Remove pnpm

* Remove from ci

* Remove pnpm

* Remove cache from lint
2024-04-06 12:39:02 -04:00
Graham Neubig 8f097f8643 Make poetry install manual and provide user with install instructions (#818)
* Add install instructions for poetry

* Update ci

* Move poetry before docker pull

* Added link
2024-04-06 12:38:12 -04:00
Junyang Lin 49f3665a99 update readme with discord server invite link (#828) 2024-04-07 00:35:46 +08:00
Leo 0b5eea967f fix: enable custom model. (#825) 2024-04-06 10:23:48 -04:00
Leo c34517be2b fix: wrong setting for localstorage workspace. (#821) 2024-04-06 08:38:12 -04:00
RaGe 228889c50c fix frontend dependencies (#810)
fixes #805
2024-04-06 07:04:10 -04:00
Anas DORBANI d38113cead Ad/fix pre commit (#807)
* Fix pre-commit config and some mypy issues

* remove hook of requirements.txt
2024-04-06 03:48:30 +00:00
Anas DORBANI 66cd9f9bd9 Check python npm pnpm docker (#800) 2024-04-06 01:20:47 +00:00
Anas DORBANI 7d3cd06ef5 Add litellm Gemini Pre-requisites (#798) 2024-04-06 00:36:34 +00:00
Alex Bäuerle d20f532289 feat: add tree view for the files in the current workspace (#601)
* feat: add tree view for the files in the current workspace

* minor

* rest endpoints

* minor
2024-04-05 20:21:39 -04:00
Anas DORBANI 7cc58b28a5 Fix pre-commit unset when using make build (#796) 2024-04-06 00:11:12 +00:00
Sagar Shah 2855959c76 translate to english (#675) 2024-04-05 13:42:41 -05:00
Robert Brennan fad51a6141 Add GitHub Action for pytest (#400)
* add action for pytest

* Update .github/workflows/run-tests.yml

* Update .github/workflows/run-tests.yml

* Update run-tests.yml

* Apply suggestions from code review

* Update run-tests.yml

* Update Pipfile

* Update .github/workflows/run-tests.yml

* Update .github/workflows/run-tests.yml

* Update .github/workflows/run-tests.yml

* Stubborn pipenv (#620)

* Update run-tests.yml

* Update run-tests.yml

* switch to poetry

* Update .github/workflows/run-tests.yml

---------

Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2024-04-05 13:24:49 -05:00
Davide Guidotti 3da56d3b6f Add the ability to set LLM_BASE_URL with make setup-config (#616)
* Add the ability to set LLM_BASE_URL with make setup-config

* Add hint for LLM_BASE_URL in make setup-config

* Adjust indentation after merge

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-05 12:57:56 -05:00
Jack Quimby d6128941b7 Doc: Document difference between agents (#722)
* doc: Guide for using local LLM with Ollama

* forgot to delete print statement

* typos

* Updated guide - new working method

* Move to docs folder

* Fixed front end overwrite local model name

* Update llm.py

* Delete docs/examples/images/example.png

deleted example.png

* Documentation of agent differences

* rename examples to documentation

* Docstrings for all agents

* typo fix

* typo fixes

* Typo fixes

* more typo fixes

* typo fix

* typo fixes

* typos fixed

* Typo fixes

* top 10 list

* typo fix

* typo fix

* typos to the moon

* typos fixed

* typo fix

* typo fix

* anotha one

* The rest of the typos

* Corrected agent descriptions

* Agents markdown updated

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-04-05 12:51:25 -05:00
Alex Bäuerle a82e065f56 feat: add commands for swebench (#682)
* feat: add commands for swebench

* restructure
2024-04-05 12:47:32 -05:00
Vincent da12d70bc2 fix to pass api key to openai embedding (#754) 2024-04-05 12:28:10 -05:00
Alex Bäuerle a202b2550f fix: show code tab by default, planner not working yet (#775)
* fix: show code tab by default, planner not working yet

* remove planner content

* remove planner for now
2024-04-05 12:26:46 -05:00
Leo adbcfefd8c feat: websocket connection management and sandbox bound to session. (#559)
* feat: websocket connection management and sandbox bound to session.

* fix: set default value to id

* feat: add session management.

* fix for mypy

* fix for mypy

* fix the pnpm-lock.

* fix the default model is empty will throw error.
2024-04-05 12:19:52 -05:00
Robert Brennan fe9815d57b Add Contributor Covenant (#769)
* Add Contributor Covenant

* Update CodeOfConduct.md
2024-04-05 13:07:59 -04:00
RaGe 89dc78953e (fix) bump actions versions (#776) 2024-04-05 12:06:01 -05:00
Alex Bäuerle 9109344728 fix: fix chat overflow (#773) 2024-04-05 12:02:24 -05:00
geohotstan 1328f55dcd Added tests for other action subclasses in test_action_serialization (#593)
* tests

* change some args

* use cls instead
2024-04-05 09:53:29 -05:00
165 changed files with 10227 additions and 13596 deletions
+5
View File
@@ -0,0 +1,5 @@
frontend/node_modules
config.toml
.envrc
.env
.git
+4 -3
View File
@@ -16,9 +16,11 @@ assignees: ''
```bash
```
**My operating system**:
<!-- tell us everything about your environment -->
**My config.toml and environment vars** (be sure to redact API keys):
```toml
**My environment vars and other configuration** (be sure to redact API keys):
```bash
```
**My model and agent** (you can see these settings in the UI):
@@ -37,4 +39,3 @@ assignees: ''
**Logs, error messages, and screenshots**:
#### Additional Context
+16
View File
@@ -0,0 +1,16 @@
---
name: Question
about: Use this template to ask a question regarding the project.
title: ''
labels: question
assignees: ''
---
## Describe your question
<!--A clear and concise description of what you want to know.-->
## Additional context
<!--Add any other context about the question here, like what you've tried so far.-->
+37 -7
View File
@@ -3,15 +3,45 @@ name: Build & Run Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
on-macos:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Run tests
python-version: ${{ matrix.python-version }}
- name: Install & Start Docker
run: |
make build
poetry run pytest ./tests
brew install colima docker
colima start
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
version: latest
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest ./tests
on-linux:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python3 -
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest ./tests
+109
View File
@@ -0,0 +1,109 @@
name: Use OpenDevin to Resolve GitHub Issue
on:
issues:
types: [labeled]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
open-devin:
if: github.event.label.name == 'dogfood-this'
runs-on: ubuntu-latest
container:
image: ghcr.io/opendevin/opendevin
volumes:
- /var/run/docker.sock:/var/run/docker.sock
steps:
- name: install git, github cli
run: apt-get install -y git gh
- name: Checkout Repository
uses: actions/checkout@v4
- name: Write Task File
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
echo "TITLE:" > task.txt
echo "${ISSUE_TITLE}" >> task.txt
echo "" >> task.txt
echo "BODY:" >> task.txt
echo "${ISSUE_BODY}" >> task.txt
- name: Run OpenDevin
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SANDBOX_TYPE: exec
run: |
python ./opendevin/main.py -d "./" -i 50 -f task.txt -d $GITHUB_WORKSPACE
rm task.txt
- name: Setup Git, Create Branch, and Commit Changes
run: |
# Setup Git configuration
git config --global --add safe.directory $PWD
git config --global user.name 'OpenDevin'
git config --global user.email 'OpenDevin@users.noreply.github.com'
# Create a unique branch name with a timestamp
BRANCH_NAME="fix/${{ github.event.issue.number }}-$(date +%Y%m%d%H%M%S)"
# Checkout new branch
git checkout -b $BRANCH_NAME
# Add all changes to staging, except task.txt
git add --all -- ':!task.txt'
# Commit the changes, if any
git commit -m "OpenDevin: Resolve Issue #${{ github.event.issue.number }}"
if [ $? -ne 0 ]; then
echo "No changes to commit."
exit 0
fi
# Push changes
git push --set-upstream origin $BRANCH_NAME
- name: Fetch Default Branch
env:
GH_TOKEN: ${{ github.token }}
run: |
# Fetch the default branch using gh cli
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
echo "Default branch is $DEFAULT_BRANCH"
echo "DEFAULT_BRANCH=$DEFAULT_BRANCH" >> $GITHUB_ENV
- name: Generate PR
env:
GH_TOKEN: ${{ github.token }}
run: |
# Create PR and capture URL
PR_URL=$(gh pr create \
--title "OpenDevin: Resolve Issue #2" \
--body "This PR was generated by OpenDevin to resolve issue #2" \
--repo "foragerr/OpenDevin" \
--head "${{ github.head_ref }}" \
--base "${{ env.DEFAULT_BRANCH }}" \
| grep -o 'https://github.com/[^ ]*')
# Extract PR number from URL
PR_NUMBER=$(echo "$PR_URL" | grep -o '[0-9]\+$')
# Set environment vars
echo "PR_URL=$PR_URL" >> $GITHUB_ENV
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
- name: Post Comment
env:
GH_TOKEN: ${{ github.token }}
run: |
gh issue comment ${{ github.event.issue.number }} \
-b "OpenDevin raised [PR #${{ env.PR_NUMBER }}](${{ env.PR_URL }}) to resolve this issue."
+6 -30
View File
@@ -1,8 +1,7 @@
name: Build and publish multi-arch container images
name: Publish Docker Image
on:
push:
branches: [ main ]
workflow_dispatch:
inputs:
reason:
@@ -14,6 +13,9 @@ jobs:
ghcr_build_and_push:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.inputs.reason != ''
strategy:
matrix:
image: ["app", "evaluation", "sandbox"]
steps:
- name: checkout
@@ -29,31 +31,5 @@ jobs:
- name: Log-in to ghcr.io
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push multi-arch container images
run: |
# set env for fork repo
DOCKER_BUILD_ORG=$(echo "${{ github.repository }}" | tr '[A-Z]' '[a-z]' | cut -d '/' -f 1)
# Find directories containing Dockerfile but not containing .dockerfileignore
while IFS= read -r dockerfile_dir; do
# Check if .dockerfileignore exists in the directory
if [ -f "$dockerfile_dir/.dockerfileignore" ]; then
echo "$dockerfile_dir/.dockerfileignore exists, skipping build and push"
continue
fi
# Check if image was already exist in ghcr.io
pushd "$dockerfile_dir" > /dev/null
FULL_IMAGE=$(make get-full-image DOCKER_BUILD_ORG=$DOCKER_BUILD_ORG)
popd > /dev/null
EXISTS=$(docker manifest inspect "$FULL_IMAGE" > /dev/null 2>&1 && echo "true" || echo "false")
if [ "$EXISTS" == "true" ]; then
echo "Image $FULL_IMAGE already exists in ghcr.io, skipping build and push"
continue
fi
# Build and push the image to ghcr.io
pushd "$dockerfile_dir" > /dev/null
make all DOCKER_BUILD_ORG=$DOCKER_BUILD_ORG
popd > /dev/null
done < <(find . -type f -name Dockerfile -exec dirname {} \; | sort -u)
- name: Build and push ${{ matrix.image }}
run: ./containers/build.sh ${{ matrix.image }} --push
+10 -19
View File
@@ -6,42 +6,33 @@ jobs:
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install PNPM
uses: pnpm/action-setup@v2
with:
package_json_file: frontend/package.json
- uses: actions/checkout@v4
- name: Install Node.js 20
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'frontend/pnpm-lock.yaml'
- name: Install dependencies
run: |
cd frontend
pnpm install --frozen-lockfile
npm install --frozen-lockfile
- name: Lint
run: |
cd frontend
pnpm run lint
npm run lint
lint-python:
name: Lint python
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install dependencies
run: pip install ruff mypy
- name: Run ruff
run: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
- name: Run mypy
run: mypy --install-types --non-interactive --config-file dev_config/python/mypy.ini opendevin/ agenthub/
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Run pre-commit hooks
run: pre-commit run --files opendevin/**/* agenthub/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
+20
View File
@@ -0,0 +1,20 @@
name: Run Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install --without evaluation
- name: Run tests
run: |
poetry run pytest ./tests
+29
View File
@@ -0,0 +1,29 @@
name: 'Close stale issues'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
# Aggressively close issues that have been explicitly labeled `age-out`
any-of-labels: age-out
stale-issue-message: 'This issue is stale because it has been open for 7 days with no activity. Remove stale label or comment or this will be closed in 1 day.'
close-issue-message: 'This issue was closed because it has been stalled for over 7 days with no activity.'
stale-pr-message: 'This PR is stale because it has been open for 7 days with no activity. Remove stale label or comment or this will be closed in 1 days.'
close-pr-message: 'This PR was closed because it has been stalled for over 7 days with no activity.'
days-before-stale: 7
days-before-close: 1
- uses: actions/stale@v9
with:
# Be more lenient with other issues
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-stale: 30
days-before-close: 7
+1
View File
@@ -198,6 +198,7 @@ logs
.envrc
/workspace
/debug
cache
# configuration
config.toml
+133
View File
@@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@rbren.io
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+66
View File
@@ -0,0 +1,66 @@
# Development Guide
This guide is for people working on OpenDevin and editing the source code.
## Start the server for development
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install)
* [Docker](https://docs.docker.com/engine/install/)(For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
* [Python](https://www.python.org/downloads/) >= 3.11
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
Make sure you have all these dependencies installed before moving on to `make build`.
### 2. Build and Setup The Environment
- **Build the Project:** Begin by building the project, which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run smoothly on your system.
```bash
make build
```
### 3. Configuring the Language Model
OpenDevin supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest.
To configure the LM of your choice, follow these steps:
1. **Using the Makefile: The Effortless Approach**
With a single command, you can have a smooth LM setup for your OpenDevin experience. Simply run:
```bash
make setup-config
```
This command will prompt you to enter the LLM API key and model name, ensuring that OpenDevin is tailored to your specific needs.
**Note on Alternative Models:**
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest. And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers).
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
### 4. Run the Application
- **Run the Application:** Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin without any hassle.
```bash
make run
```
### 5. Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related components or interface enhancements.
```bash
make start-frontend
```
### 6. Help
- **Get Some Help:** Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
```bash
make help
```
+129 -41
View File
@@ -6,57 +6,144 @@ BACKEND_PORT = 3000
BACKEND_HOST = "127.0.0.1:$(BACKEND_PORT)"
FRONTEND_PORT = 3001
DEFAULT_WORKSPACE_DIR = "./workspace"
DEFAULT_MODEL = "gpt-4-0125-preview"
DEFAULT_MODEL = "gpt-3.5-turbo-1106"
CONFIG_FILE = config.toml
PRECOMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
# ANSI color codes
GREEN=\033[0;32m
YELLOW=\033[0;33m
RED=\033[0;31m
BLUE=\033[0;34m
RESET=\033[0m
# Build
build:
@echo "Building project..."
@echo "Pulling Docker image..."
@docker pull $(DOCKER_IMAGE)
@echo "Installing Python dependencies..."
@curl -sSL https://install.python-poetry.org | python3 -
@poetry install --without evaluation
@echo "Activating Poetry shell..."
@echo "Installing pre-commit hooks..."
@poetry run pre-commit install --config $(PRECOMMIT_CONFIG_PATH)
@echo "Setting up frontend environment..."
@echo "Detect Node.js version..."
@cd frontend && node ./scripts/detect-node-version.js
@cd frontend && if [ -f node_modules/.package-lock.json ]; then \
echo "This project currently uses \"pnpm\" for dependency management. It has detected that dependencies were previously installed using \"npm\" and has automatically deleted the \"node_modules\" directory to prevent unnecessary conflicts."; \
rm -rf node_modules; \
@echo "$(GREEN)Building project...$(RESET)"
@$(MAKE) -s check-dependencies
@$(MAKE) -s pull-docker-image
@$(MAKE) -s install-python-dependencies
@$(MAKE) -s install-frontend-dependencies
@$(MAKE) -s install-precommit-hooks
@$(MAKE) -s build-frontend
@echo "$(GREEN)Build completed successfully.$(RESET)"
check-dependencies:
@echo "$(YELLOW)Checking dependencies...$(RESET)"
@$(MAKE) -s check-python
@$(MAKE) -s check-npm
@$(MAKE) -s check-docker
@$(MAKE) -s check-poetry
@echo "$(GREEN)Dependencies checked successfully.$(RESET)"
check-python:
@echo "$(YELLOW)Checking Python installation...$(RESET)"
@if command -v python3 > /dev/null; then \
echo "$(BLUE)$(shell python3 --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Python 3 is not installed. Please install Python 3 to continue.$(RESET)"; \
exit 1; \
fi
@which corepack > /dev/null || (echo "Installing corepack..." && npm install -g corepack)
@cd frontend && sudo corepack enable && pnpm install && pnpm run make-i18n
check-npm:
@echo "$(YELLOW)Checking npm installation...$(RESET)"
@if command -v npm > /dev/null; then \
echo "$(BLUE)npm $(shell npm --version) is already installed.$(RESET)"; \
else \
echo "$(RED)npm is not installed. Please install Node.js to continue.$(RESET)"; \
exit 1; \
fi
check-docker:
@echo "$(YELLOW)Checking Docker installation...$(RESET)"
@if command -v docker > /dev/null; then \
echo "$(BLUE)$(shell docker --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Docker is not installed. Please install Docker to continue.$(RESET)"; \
exit 1; \
fi
check-poetry:
@echo "$(YELLOW)Checking Poetry installation...$(RESET)"
@if command -v poetry > /dev/null; then \
echo "$(BLUE)$(shell poetry --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Poetry is not installed. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python3 -$(RESET)"; \
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
exit 1; \
fi
pull-docker-image:
@echo "$(YELLOW)Pulling Docker image...$(RESET)"
@docker pull $(DOCKER_IMAGE)
@echo "$(GREEN)Docker image pulled successfully.$(RESET)"
install-python-dependencies:
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
@if [ "$(shell uname)" = "Darwin" ]; then \
echo "$(BLUE)Installing `chroma-hnswlib`...$(RESET)"; \
export HNSWLIB_NO_NATIVE=1; \
poetry run pip install chroma-hnswlib; \
fi
@poetry install --without evaluation
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
install-frontend-dependencies:
@echo "$(YELLOW)Setting up frontend environment...$(RESET)"
@echo "$(YELLOW)Detect Node.js version...$(RESET)"
@cd frontend && node ./scripts/detect-node-version.js
@cd frontend && \
echo "$(BLUE)Installing frontend dependencies with npm...$(RESET)" && \
npm install && \
echo "$(BLUE)Running make-i18n with npm...$(RESET)" && \
npm run make-i18n
@echo "$(GREEN)Frontend dependencies installed successfully.$(RESET)"
install-precommit-hooks:
@echo "$(YELLOW)Installing pre-commit hooks...$(RESET)"
@git config --unset-all core.hooksPath || true
@poetry run pre-commit install --config $(PRECOMMIT_CONFIG_PATH)
@echo "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
build-frontend:
@echo "$(YELLOW)Building frontend...$(RESET)"
@cd frontend && npm run build
# Start backend
start-backend:
@echo "Starting backend..."
@echo "$(YELLOW)Starting backend...$(RESET)"
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT)
# Start frontend
start-frontend:
@echo "Starting frontend..."
@cd frontend && BACKEND_HOST=$(BACKEND_HOST) FRONTEND_PORT=$(FRONTEND_PORT) pnpm run start
@echo "$(YELLOW)Starting frontend...$(RESET)"
@cd frontend && BACKEND_HOST=$(BACKEND_HOST) FRONTEND_PORT=$(FRONTEND_PORT) npm run start
# Run the app
run:
@echo "Running the app..."
@echo "$(YELLOW)Running the app...$(RESET)"
@if [ "$(OS)" = "Windows_NT" ]; then \
echo "`make run` is not supported on Windows. Please run `make start-frontend` and `make start-backend` separately."; \
echo "$(RED)`make run` is not supported on Windows. Please run `make start-frontend` and `make start-backend` separately.$(RESET)"; \
exit 1; \
fi
@mkdir -p logs
@poetry run nohup uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) > logs/backend_$(shell date +'%Y%m%d_%H%M%S').log 2>&1 &
@echo "Waiting for the backend to start..."
@echo "$(YELLOW)Starting backend server...$(RESET)"
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) &
@echo "$(YELLOW)Waiting for the backend to start...$(RESET)"
@until nc -z localhost $(BACKEND_PORT); do sleep 0.1; done
@cd frontend && pnpm run start -- --port $(FRONTEND_PORT)
@echo "$(GREEN)Backend started successfully.$(RESET)"
@cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run start -- --port $(FRONTEND_PORT)
@echo "$(GREEN)Application started successfully.$(RESET)"
# Setup config.toml
setup-config:
@echo "Setting up config.toml..."
@echo "$(YELLOW)Setting up config.toml...$(RESET)"
@$(MAKE) setup-config-prompts
@mv $(CONFIG_FILE).tmp $(CONFIG_FILE)
@echo "$(GREEN)Config.toml setup completed.$(RESET)"
setup-config-prompts:
@read -p "Enter your LLM Model name (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
llm_model=$${llm_model:-$(DEFAULT_MODEL)}; \
echo "LLM_MODEL=\"$$llm_model\"" > $(CONFIG_FILE).tmp
@@ -64,14 +151,17 @@ setup-config:
@read -p "Enter your LLM API key: " llm_api_key; \
echo "LLM_API_KEY=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM Base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
if [[ ! -z "$$llm_base_url" ]]; then echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@echo "Enter your LLM Embedding Model\nChoices are openai, azureopenai, llama2 or leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "LLM_EMBEDDING_MODEL=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
if [ "$$llm_embedding_model" = "llama2" ]; then \
read -p "Enter the local model URL: " llm_base_url; \
read -p "Enter the local model URL (will overwrite LLM_BASE_URL): " llm_base_url; \
echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
read -p "Enter the Azure endpoint URL: " llm_base_url; \
read -p "Enter the Azure endpoint URL (will overwrite LLM_BASE_URL): " llm_base_url; \
echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure LLM Deployment Name: " llm_deployment_name; \
echo "LLM_DEPLOYMENT_NAME=\"$$llm_deployment_name\"" >> $(CONFIG_FILE).tmp; \
@@ -81,22 +171,20 @@ setup-config:
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "WORKSPACE_DIR=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
@mv $(CONFIG_FILE).tmp $(CONFIG_FILE)
echo "WORKSPACE_BASE=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
# Help
help:
@echo "Usage: make [target]"
@echo "$(BLUE)Usage: make [target]$(RESET)"
@echo "Targets:"
@echo " build - Build project, including environment setup and dependencies."
@echo " build-eval - Build project evaluation pipeline, including environment setup and dependencies."
@echo " start-backend - Start the backend server for the OpenDevin project."
@echo " start-frontend - Start the frontend server for the OpenDevin project."
@echo " run - Run the OpenDevin application, starting both backend and frontend servers."
@echo " $(GREEN)build$(RESET) - Build project, including environment setup and dependencies."
@echo " $(GREEN)setup-config$(RESET) - Setup the configuration for OpenDevin by providing LLM API key,"
@echo " LLM Model name, and workspace directory."
@echo " $(GREEN)start-backend$(RESET) - Start the backend server for the OpenDevin project."
@echo " $(GREEN)start-frontend$(RESET) - Start the frontend server for the OpenDevin project."
@echo " $(GREEN)run$(RESET) - Run the OpenDevin application, starting both backend and frontend servers."
@echo " Backend Log file will be stored in the 'logs' directory."
@echo " setup-config - Setup the configuration for OpenDevin by providing LLM API key, LLM Model name, and workspace directory."
@echo " help - Display this help message, providing information on available targets."
@echo " $(GREEN)help$(RESET) - Display this help message, providing information on available targets."
# Phony targets
.PHONY: build build-eval start-backend start-frontend run setup-config help
.PHONY: build check-dependencies check-python check-npm check-docker check-poetry pull-docker-image install-python-dependencies install-frontend-dependencies install-precommit-hooks start-backend start-frontend run setup-config setup-config-prompts help
+50 -56
View File
@@ -24,6 +24,9 @@
<a href="https://github.com/OpenDevin/OpenDevin/stargazers"><img src="https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge" alt="Stargazers"></a>
<a href="https://github.com/OpenDevin/OpenDevin/issues"><img src="https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge" alt="Issues"></a>
<a href="https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge" alt="MIT License"></a>
</br>
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2etftj1dd-X1fDL2PYIVpsmJZkqEYANw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/mBuDGRzzES"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
</div>
<!-- PROJECT LOGO -->
@@ -108,75 +111,61 @@ After completing the MVP, the team will focus on research in various areas, incl
</a>
</p>
## ⚠️ Caveats and Warnings
* OpenDevin is still an alpha project. It is changing very quickly and is unstable. We are working on getting a stable release out in the coming weeks.
* OpenDevin will issue many prompts to the LLM you configure. Most of these LLMs cost money--be sure to set spending limits and monitor usage.
* OpenDevin runs `bash` commands within a Docker sandbox, so it should not affect your machine. But your workspace directory will be attached to that sandbox, and files in the directory may be modified or deleted.
* Our default Agent is currently the MonologueAgent, which has limited capabilities, but is fairly stable. We're working on other Agent implementations, including [SWE Agent](https://swe-agent.com/). You can [read about our current set of agents here](./docs/documentation/Agents.md).
## 🚀 Get Started
Getting started with the OpenDevin project is incredibly easy. Follow these simple steps to set up and run OpenDevin on your system:
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install)
* [Docker](https://docs.docker.com/engine/install/)(For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
* [Python](https://www.python.org/downloads/) >= 3.11
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
The easiest way to run OpenDevin is inside a Docker container.
You can run:
```bash
# Your OpenAI API key, or any other LLM API key
export LLM_API_KEY="sk-..."
### 2. Build and Setup The Environment
# The directory you want OpenDevin to modify. MUST be an absolute path!
export WORKSPACE_DIR=$(pwd)/workspace
- **Build the Project:** Begin by building the project, which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run smoothly on your system.
```bash
make build
```
docker run \
-e LLM_API_KEY \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_DIR \
-v $WORKSPACE_DIR:/opt/workspace_base \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
ghcr.io/opendevin/opendevin:latest
```
Replace `$(pwd)/workspace` with the path to the code you want OpenDevin to work with.
### 3. Configuring the Language Model
You can find opendevin running at `http://localhost:3000`.
OpenDevin supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest.
See [Development.md](Development.md) for instructions on running OpenDevin without Docker.
To configure the LM of your choice, follow these steps:
## 🤖 LLM Backends
OpenDevin can work with any LLM backend.
For a full list of the LM providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).
1. **Using the Makefile: The Effortless Approach**
With a single command, you can have a smooth LM setup for your OpenDevin experience. Simply run:
```bash
make setup-config
```
This command will prompt you to enter the LLM API key and model name, ensuring that OpenDevin is tailored to your specific needs.
The `LLM_MODEL` environment variable controls which model is used in programmatic interactions,
but choosing a model in the OpenDevin UI will override this setting.
2. **Manual Config: The Artisanal Touch**
If you're feeling particularly adventurous, you can manually update the `config.toml` file located in the project's root directory. Here, you'll find the `llm_api_key` and `llm_model_name` fields, where you can set the LM of your choosing.
The following environment variables might be necessary for some LLMs:
* `LLM_API_KEY`
* `LLM_BASE_URL`
* `LLM_EMBEDDING_MODEL`
* `LLM_DEPLOYMENT_NAME`
* `LLM_API_VERSION`
**Note on Alternative Models:**
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest. And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
Some alternative models may prove more challenging to tame than others.
Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest.
And if you've already mastered the art of wielding a model other than OpenAI's GPT,
we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers).
### 4. Run the Application
- **Run the Application:** Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin without any hassle.
```bash
make run
```
### 5. Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related components or interface enhancements.
```bash
make start-frontend
```
### 6. Help
- **Get Some Help:** Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
```bash
make help
```
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
## ⭐️ Research Strategy
@@ -211,7 +200,12 @@ For details, please check [this document](./CONTRIBUTING.md).
## 🤖 Join Our Community
Join our Slack workspace by filling out the [form](https://forms.gle/758d5p6Ve8r2nxxq6). Stay updated on OpenDevin's progress, share ideas, and collaborate with fellow enthusiasts and experts. Let's simplify software engineering together!
Now we have both Slack workspace for the collaboration on building OpenDevin and Discord server for discussion about anything related, e.g., this project, LLM, agent, etc.
* [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2etftj1dd-X1fDL2PYIVpsmJZkqEYANw)
* [Discord server](https://discord.gg/mBuDGRzzES)
If you would love to contribute, feel free to join our community (note that now there is no need to fill in the [form](https://forms.gle/758d5p6Ve8r2nxxq6)). Let's simplify software engineering together!
🐚 **Code less, make more with OpenDevin.**
+3 -3
View File
@@ -2,8 +2,8 @@ from dotenv import load_dotenv
load_dotenv()
# Import agents after environment variables are loaded
from . import monologue_agent # noqa: E402
from . import codeact_agent # noqa: E402
from . import planner_agent # noqa: E402
from . import monologue_agent # noqa: E402
from . import codeact_agent # noqa: E402
from . import planner_agent # noqa: E402
__all__ = ['monologue_agent', 'codeact_agent', 'planner_agent']
+6 -4
View File
@@ -1,21 +1,23 @@
# CodeAct-based Agent Framework
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
A minimalistic example can be found at [research/codeact/examples/run_flask_server_with_bash.py](./examples/run_flask_server_with_bash.py):
**NOTE: This agent is still highly experimental and under active development to reach the capability described in the original paper & [repo](https://github.com/xingyaoww/code-act).**
<video src="https://github.com/xingyaoww/code-act/assets/38853559/62c80ada-62ce-447e-811c-fc801dd4beac"> </video>
*Demo of the expected capability - work-in-progress.*
```bash
mkdir workspace
PYTHONPATH=`pwd`:$PYTHONPATH python3 opendevin/main.py -d ./workspace -c CodeActAgent -t "Please write a flask app that returns 'Hello, World\!' at the root URL, then start the app on port 5000. python3 has already been installed for you."
```
Example: prompts `gpt-4-0125-preview` to write a flask server, install `flask` library, and start the server.
<img width="951" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/325c3115-a343-4cc5-a92b-f1e5d552a077">
<img width="957" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/68ad10c1-744a-4e9d-bb29-0f163d665a0a">
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
**TODO**: This should be fixable by either (1) including a complete in-context example like [this](https://github.com/xingyaoww/mint-bench/blob/main/mint/tasks/in_context_examples/reasoning/with_tool.txt), OR (2) collect some interaction data like this and fine-tune a model (like [this](https://github.com/xingyaoww/code-act), a more complex route).
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.agent import Agent
from .codeact_agent import CodeActAgent
Agent.register("CodeActAgent", CodeActAgent)
Agent.register('CodeActAgent', CodeActAgent)
+76 -38
View File
@@ -1,23 +1,32 @@
import re
from typing import List, Mapping
from opendevin.agent import Agent
from opendevin.state import State
from opendevin.action import (
Action,
CmdRunAction,
AgentEchoAction,
AgentFinishAction,
CmdRunAction,
)
from opendevin.observation import (
CmdOutputObservation,
AgentMessageObservation,
)
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentMessageObservation,
CmdOutputObservation,
)
from opendevin.parse_commands import parse_command_file
from opendevin.state import State
SYSTEM_MESSAGE = """You are a helpful assistant. You will be provided access (as root) to a bash shell to complete user-provided tasks.
COMMAND_DOCS = parse_command_file()
COMMAND_SEGMENT = (
f"""
Apart from the standard bash commands, you can also use the following special commands:
{COMMAND_DOCS}
"""
if COMMAND_DOCS is not None
else ''
)
SYSTEM_MESSAGE = f"""You are a helpful assistant. You will be provided access (as root) to a bash shell to complete user-provided tasks.
You will be able to execute commands in the bash shell, interact with the file system, install packages, and receive the output of your commands.
DO NOT provide code in ```triple backticks```. Instead, you should execute bash command on behalf of the user by wrapping them with <execute> and </execute>.
@@ -34,25 +43,32 @@ You can also write a block of code to a file:
echo "import math
print(math.pi)" > math.py
</execute>
{COMMAND_SEGMENT}
When you are done, execute "exit" to close the shell and end the conversation.
When you are done, execute the following to close the shell and end the conversation:
<execute>exit</execute>
"""
INVALID_INPUT_MESSAGE = (
"I don't understand your input. \n"
"If you want to execute command, please use <execute> YOUR_COMMAND_HERE </execute>.\n"
"If you already completed the task, please exit the shell by generating: <execute> exit </execute>."
'If you want to execute command, please use <execute> YOUR_COMMAND_HERE </execute>.\n'
'If you already completed the task, please exit the shell by generating: <execute> exit </execute>.'
)
def parse_response(response) -> str:
action = response.choices[0].message.content
if "<execute>" in action and "</execute>" not in action:
action += "</execute>"
if '<execute>' in action and '</execute>' not in action:
action += '</execute>'
return action
class CodeActAgent(Agent):
"""
The Code Act Agent is a minimalist agent.
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
"""
def __init__(
self,
llm: LLM,
@@ -61,47 +77,69 @@ class CodeActAgent(Agent):
Initializes a new instance of the CodeActAgent class.
Parameters:
- instruction (str): The instruction for the agent to execute.
- max_steps (int): The maximum number of steps to run the agent.
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.messages: List[Mapping[str, str]] = []
def step(self, state: State) -> Action:
"""
Performs one step using the Code Act Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
Parameters:
- state (State): used to get updated info and background commands
Returns:
- CmdRunAction(command) - command action to run
- AgentEchoAction(content=INVALID_INPUT_MESSAGE) - invalid command output
Raises:
- NotImplementedError - for actions other than CmdOutputObservation or AgentMessageObservation
"""
if len(self.messages) == 0:
assert state.plan.main_goal, "Expecting instruction to be set"
assert state.plan.main_goal, 'Expecting instruction to be set'
self.messages = [
{"role": "system", "content": SYSTEM_MESSAGE},
{"role": "user", "content": state.plan.main_goal},
{'role': 'system', 'content': SYSTEM_MESSAGE},
{'role': 'user', 'content': state.plan.main_goal},
]
updated_info = state.updated_info
if updated_info:
for prev_action, obs in updated_info:
assert isinstance(prev_action, (CmdRunAction, AgentEchoAction)), "Expecting CmdRunAction or AgentEchoAction for Action"
if isinstance(obs, AgentMessageObservation): # warning message from itself
self.messages.append({"role": "user", "content": obs.content})
assert isinstance(
prev_action, (CmdRunAction, AgentEchoAction)
), 'Expecting CmdRunAction or AgentEchoAction for Action'
if isinstance(
obs, AgentMessageObservation
): # warning message from itself
self.messages.append(
{'role': 'user', 'content': obs.content})
elif isinstance(obs, CmdOutputObservation):
content = "OBSERVATION:\n" + obs.content
content += f"\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]"
self.messages.append({"role": "user", "content": content})
content = 'OBSERVATION:\n' + obs.content
content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
self.messages.append({'role': 'user', 'content': content})
else:
raise NotImplementedError(f"Unknown observation type: {obs.__class__}")
raise NotImplementedError(
f'Unknown observation type: {obs.__class__}'
)
response = self.llm.completion(
messages=self.messages,
stop=["</execute>"],
temperature=0.0,
seed=42,
stop=['</execute>'],
temperature=0.0
)
action_str: str = parse_response(response)
self.messages.append({"role": "assistant", "content": action_str})
state.num_of_chars += sum(len(message['content'])
for message in self.messages) + len(action_str)
self.messages.append({'role': 'assistant', 'content': action_str})
command = re.search(r"<execute>(.*)</execute>", action_str, re.DOTALL)
command = re.search(r'<execute>(.*)</execute>', action_str, re.DOTALL)
if command is not None:
# a command was found
command_group = command.group(1)
if command_group.strip() == "exit":
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command = command_group)
return CmdRunAction(command=command_group)
# # execute the code
# # TODO: does exit_code get loaded into Message?
# exit_code, observation = self.env.execute(command_group)
@@ -111,9 +149,9 @@ class CodeActAgent(Agent):
# https://github.com/xingyaoww/mint-bench/blob/main/mint/envs/general_env.py#L18-L23
# observation = INVALID_INPUT_MESSAGE
# self._history.append(Message(Role.ASSISTANT, observation))
return AgentEchoAction(content=INVALID_INPUT_MESSAGE) # warning message to itself
return AgentEchoAction(
content=INVALID_INPUT_MESSAGE
) # warning message to itself
def search_memory(self, query: str) -> List[str]:
raise NotImplementedError("Implement this abstract method")
raise NotImplementedError('Implement this abstract method')
-1
View File
@@ -6,4 +6,3 @@ There's a lot of low-hanging fruit for this agent:
* Improve memory condensing--condense earlier memories more aggressively
* Limit the time that `run` can wait (in case agent runs an interactive command and it's hanging)
* Figure out how to run background processes, e.g. `node server.js` to start a server
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.agent import Agent
from .agent import MonologueAgent
Agent.register("MonologueAgent", MonologueAgent)
Agent.register('MonologueAgent', MonologueAgent)
+125 -62
View File
@@ -2,6 +2,8 @@ from typing import List
from opendevin.agent import Agent
from opendevin.state import State
from opendevin.llm.llm import LLM
from opendevin.schema import ActionType, ObservationType
from opendevin.exceptions import AgentNoInstructionError
from opendevin.action import (
Action,
@@ -31,123 +33,174 @@ MAX_MONOLOGUE_LENGTH = 20000
MAX_OUTPUT_LENGTH = 5000
INITIAL_THOUGHTS = [
"I exist!",
"Hmm...looks like I can type in a command line prompt",
"Looks like I have a web browser too!",
'I exist!',
'Hmm...looks like I can type in a command line prompt',
'Looks like I have a web browser too!',
"Here's what I want to do: $TASK",
"How am I going to get there though?",
"It seems like I have some kind of short term memory.",
"Each of my thoughts seems to be stored in a JSON array.",
"It seems whatever I say next will be added as an object to the list.",
"But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.",
"Fortunately I have long term memory!",
"I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!",
'How am I going to get there though?',
'It seems like I have some kind of short term memory.',
'Each of my thoughts seems to be stored in a JSON array.',
'It seems whatever I say next will be added as an object to the list.',
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
'Fortunately I have long term memory!',
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
"Let's try it out!",
"RECALL what it is I want to do",
'RECALL what it is I want to do',
"Here's what I want to do: $TASK",
"How am I going to get there though?",
'How am I going to get there though?',
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
'RUN echo "hello world"',
"hello world",
"Cool! I bet I can write files too using the write action.",
'hello world',
'Cool! I bet I can write files too using the write action.',
"WRITE echo \"console.log('hello world')\" > test.js",
"",
'',
"I just created test.js. I'll try and run it now.",
"RUN node test.js",
"hello world",
"It works!",
'RUN node test.js',
'hello world',
'It works!',
"I'm going to try reading it now using the read action.",
"READ test.js",
'READ test.js',
"console.log('hello world')",
"Nice! I can read files too!",
"And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument",
'Nice! I can read files too!',
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
"Let's try that...",
"BROWSE google.com",
'BROWSE google.com',
'<form><input type="text"></input><button type="submit"></button></form>',
"I can browse the web too!",
"And once I have completed my task, I can use the finish action to stop working.",
'I can browse the web too!',
'And once I have completed my task, I can use the finish action to stop working.',
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
"Very cool. Now to accomplish my task.",
'Very cool. Now to accomplish my task.',
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
"In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.",
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
"It seems like there might be an existing project here. I should probably start by running `ls` to see what's here.",
]
class MonologueAgent(Agent):
"""
The Monologue Agent utilizes long and short term memory to complete tasks.
Long term memory is stored as a LongTermMemory object and the model uses it to search for examples from the past.
Short term memory is stored as a Monologue object and the model can condense it as necessary.
"""
_initialized = False
def __init__(self, llm: LLM):
"""
Initializes the Monologue Agent with an llm, monologue, and memory.
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.monologue = Monologue()
self.memory = LongTermMemory()
def _add_event(self, event: dict):
if "extras" in event and "screenshot" in event["extras"]:
del event["extras"]["screenshot"]
if 'args' in event and 'output' in event['args'] and len(event['args']['output']) > MAX_OUTPUT_LENGTH:
event['args']['output'] = event['args']['output'][:MAX_OUTPUT_LENGTH] + "..."
"""
Adds a new event to the agent's monologue and memory.
Monologue automatically condenses when it gets too large.
Parameters:
- event (dict): The event that will be added to monologue and memory
"""
if 'extras' in event and 'screenshot' in event['extras']:
del event['extras']['screenshot']
if (
'args' in event
and 'output' in event['args']
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
):
event['args']['output'] = (
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
)
self.monologue.add_event(event)
self.memory.add_event(event)
if self.monologue.get_total_length() > MAX_MONOLOGUE_LENGTH:
self.monologue.condense(self.llm)
def _initialize(self, task):
def _initialize(self, task: str):
"""
Utilizes the INITIAL_THOUGHTS list to give the agent a context for it's capabilities and how to navigate the /workspace.
Short circuted to return when already initialized.
Parameters:
- task (str): The initial goal statement provided by the user
Raises:
- AgentNoInstructionError: If task is not provided
"""
if self._initialized:
return
if task is None or task == "":
raise ValueError("Instruction must be provided")
if task is None or task == '':
raise AgentNoInstructionError()
self.monologue = Monologue()
self.memory = LongTermMemory()
output_type = ""
output_type = ''
for thought in INITIAL_THOUGHTS:
thought = thought.replace("$TASK", task)
if output_type != "":
observation: Observation = NullObservation(content="")
if output_type == "run":
observation = CmdOutputObservation(content=thought, command_id=0, command="")
elif output_type == "read":
observation = FileReadObservation(content=thought, path="")
elif output_type == "recall":
observation = AgentRecallObservation(content=thought, memories=[])
elif output_type == "browse":
observation = BrowserOutputObservation(content=thought, url="", screenshot="")
thought = thought.replace('$TASK', task)
if output_type != '':
observation: Observation = NullObservation(content='')
if output_type == ObservationType.RUN:
observation = CmdOutputObservation(
content=thought, command_id=0, command=''
)
elif output_type == ObservationType.READ:
observation = FileReadObservation(content=thought, path='')
elif output_type == ObservationType.RECALL:
observation = AgentRecallObservation(
content=thought, memories=[])
elif output_type == ObservationType.BROWSE:
observation = BrowserOutputObservation(
content=thought, url='', screenshot=''
)
self._add_event(observation.to_dict())
output_type = ""
output_type = ''
else:
action: Action = NullAction()
if thought.startswith("RUN"):
command = thought.split("RUN ")[1]
if thought.startswith('RUN'):
command = thought.split('RUN ')[1]
action = CmdRunAction(command)
output_type = "run"
elif thought.startswith("WRITE"):
parts = thought.split("WRITE ")[1].split(" > ")
output_type = ActionType.RUN
elif thought.startswith('WRITE'):
parts = thought.split('WRITE ')[1].split(' > ')
path = parts[1]
content = parts[0]
action = FileWriteAction(path=path, content=content)
elif thought.startswith("READ"):
path = thought.split("READ ")[1]
elif thought.startswith('READ'):
path = thought.split('READ ')[1]
action = FileReadAction(path=path)
output_type = "read"
elif thought.startswith("RECALL"):
query = thought.split("RECALL ")[1]
output_type = ActionType.READ
elif thought.startswith('RECALL'):
query = thought.split('RECALL ')[1]
action = AgentRecallAction(query=query)
output_type = "recall"
elif thought.startswith("BROWSE"):
url = thought.split("BROWSE ")[1]
output_type = ActionType.RECALL
elif thought.startswith('BROWSE'):
url = thought.split('BROWSE ')[1]
action = BrowseURLAction(url=url)
output_type = "browse"
output_type = ActionType.BROWSE
else:
action = AgentThinkAction(thought=thought)
self._add_event(action.to_dict())
self._initialized = True
def step(self, state: State) -> Action:
"""
Modifies the current state by adding the most recent actions and observations, then prompts the model to think about it's next action to take using monologue, memory, and hint.
Parameters:
- state (State): The current state based on previous steps taken
Returns:
- Action: The next action to take based on LLM response
"""
self._initialize(state.plan.main_goal)
for prev_action, obs in state.updated_info:
self._add_event(prev_action.to_dict())
@@ -160,13 +213,23 @@ class MonologueAgent(Agent):
self.monologue.get_thoughts(),
state.background_commands_obs,
)
messages = [{"content": prompt,"role": "user"}]
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = prompts.parse_action_response(action_resp)
self.latest_action = action
return action
def search_memory(self, query: str) -> List[str]:
return self.memory.search(query)
"""
Uses VectorIndexRetriever to find related memories within the long term memory.
Uses search to produce top 10 results.
Parameters:
- query (str): The query that we want to find related memories for
Returns:
- List[str]: A list of top 10 text results that matched the query
"""
return self.memory.search(query)
+27 -4
View File
@@ -1,14 +1,37 @@
import json
from json_repair import repair_json
def my_encoder(obj):
if hasattr(obj, "to_dict"):
"""
Encodes objects as dictionaries
Parameters:
- obj (Object): An object that will be converted
Returns:
- dict: If the object can be converted it is returned in dict format
"""
if hasattr(obj, 'to_dict'):
return obj.to_dict()
def dumps(obj, **kwargs):
"""
Serialize an object to str format
"""
return json.dumps(obj, default=my_encoder, **kwargs)
def loads(s, **kwargs):
s_repaired = repair_json(s)
return json.loads(s_repaired, **kwargs)
def loads(s, **kwargs):
"""
Create a JSON object from str
"""
json_start = s.find('{')
json_end = s.rfind('}') + 1
if json_start == -1 or json_end == -1:
raise ValueError('Invalid response: no JSON found')
s = s[json_start:json_end]
s = repair_json(s)
return json.loads(s, **kwargs)
+59 -32
View File
@@ -5,75 +5,102 @@ from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from opendevin import config
from opendevin.logger import opendevin_logger as logger
from . import json
embedding_strategy = config.get("LLM_EMBEDDING_MODEL")
embedding_strategy = config.get('LLM_EMBEDDING_MODEL')
# TODO: More embeddings: https://docs.llamaindex.ai/en/stable/examples/embeddings/OpenAI/
# There's probably a more programmatic way to do this.
if embedding_strategy == "llama2":
if embedding_strategy == 'llama2':
from llama_index.embeddings.ollama import OllamaEmbedding
embed_model = OllamaEmbedding(
model_name="llama2",
base_url=config.get_or_error("LLM_BASE_URL"),
ollama_additional_kwargs={"mirostat": 0},
model_name='llama2',
base_url=config.get('LLM_BASE_URL', required=True),
ollama_additional_kwargs={'mirostat': 0},
)
elif embedding_strategy == "openai":
elif embedding_strategy == 'openai':
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding(
model="text-embedding-ada-002"
model='text-embedding-ada-002',
api_key=config.get('LLM_API_KEY', required=True)
)
elif embedding_strategy == "azureopenai":
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding # Need to instruct to set these env variables in documentation
elif embedding_strategy == 'azureopenai':
# Need to instruct to set these env variables in documentation
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
embed_model = AzureOpenAIEmbedding(
model="text-embedding-ada-002",
deployment_name=config.get_or_error("LLM_DEPLOYMENT_NAME"),
api_key=config.get_or_error("LLM_API_KEY"),
azure_endpoint=config.get_or_error("LLM_BASE_URL"),
api_version=config.get_or_error("LLM_API_VERSION"),
model='text-embedding-ada-002',
deployment_name=config.get('LLM_DEPLOYMENT_NAME', required=True),
api_key=config.get('LLM_API_KEY', required=True),
azure_endpoint=config.get('LLM_BASE_URL', required=True),
api_version=config.get('LLM_API_VERSION', required=True),
)
else:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-en-v1.5"
model_name='BAAI/bge-small-en-v1.5'
)
class LongTermMemory:
"""
Responsible for storing information that the agent can call on later for better insights and context.
Uses chromadb to store and search through memories.
"""
def __init__(self):
"""
Initialize the chromadb and set up ChromaVectorStore for later use.
"""
db = chromadb.Client()
self.collection = db.get_or_create_collection(name="memories")
self.collection = db.get_or_create_collection(name='memories')
vector_store = ChromaVectorStore(chroma_collection=self.collection)
self.index = VectorStoreIndex.from_vector_store(vector_store, embed_model=embed_model)
self.index = VectorStoreIndex.from_vector_store(
vector_store, embed_model=embed_model)
self.thought_idx = 0
def add_event(self, event):
id = ""
t = ""
if "action" in event:
t = "action"
id = event["action"]
elif "observation" in event:
t = "observation"
id = event["observation"]
def add_event(self, event: dict):
"""
Adds a new event to the long term memory with a unique id.
Parameters:
- event (dict): The new event to be added to memory
"""
id = ''
t = ''
if 'action' in event:
t = 'action'
id = event['action']
elif 'observation' in event:
t = 'observation'
id = event['observation']
doc = Document(
text=json.dumps(event),
doc_id=str(self.thought_idx),
extra_info={
"type": t,
"id": id,
"idx": self.thought_idx,
'type': t,
'id': id,
'idx': self.thought_idx,
},
)
self.thought_idx += 1
logger.debug("Adding %s event to memory: %d", t, self.thought_idx)
self.index.insert(doc)
def search(self, query, k=10):
def search(self, query: str, k: int = 10):
"""
Searches through the current memory using VectorIndexRetriever
Parameters:
- query (str): A query to match search results to
- k (int): Number of top results to return
Returns:
- List[str]: List of top k results found in current memory
"""
retriever = VectorIndexRetriever(
index=self.index,
similarity_top_k=k,
)
results = retriever.retrieve(query)
return [r.get_text() for r in results]
+48 -10
View File
@@ -1,40 +1,78 @@
import traceback
from opendevin.llm.llm import LLM
from opendevin.exceptions import AgentEventTypeError
import agenthub.monologue_agent.utils.json as json
import agenthub.monologue_agent.utils.prompts as prompts
class Monologue:
"""
The monologue is a representation for the agent's internal monologue where it can think.
The agent has the capability of using this monologue for whatever it wants.
"""
def __init__(self):
"""
Initialize the empty list of thoughts
"""
self.thoughts = []
def add_event(self, t: dict):
"""
Adds an event to memory if it is a valid event.
Parameters:
- t (dict): The thought that we want to add to memory
Raises:
- AgentEventTypeError: If t is not a dict
"""
if not isinstance(t, dict):
raise ValueError("Event must be a dictionary")
raise AgentEventTypeError()
self.thoughts.append(t)
def get_thoughts(self):
"""
Get the current thoughts of the agent.
Returns:
- List: The list of thoughts that the agent has.
"""
return self.thoughts
def get_total_length(self):
"""
Gives the total number of characters in all thoughts
Returns:
- Int: Total number of chars in thoughts.
"""
total_length = 0
for t in self.thoughts:
try:
total_length += len(json.dumps(t))
except TypeError as e:
print(f"Error serializing thought: {e}")
print(f'Error serializing thought: {e}')
return total_length
def condense(self, llm):
def condense(self, llm: LLM):
"""
Attempts to condense the monologue by using the llm
Parameters:
- llm (LLM): llm to be used for summarization
Raises:
- RunTimeError: When the condensing process fails for any reason
"""
try:
prompt = prompts.get_summarize_monologue_prompt(self.thoughts)
messages = [{"content": prompt,"role": "user"}]
messages = [{'content': prompt, 'role': 'user'}]
resp = llm.completion(messages=messages)
summary_resp = resp['choices'][0]['message']['content']
self.thoughts = prompts.parse_summary_response(strip_markdown(summary_resp))
self.thoughts = prompts.parse_summary_response(summary_resp)
except Exception as e:
traceback.print_exc()
raise RuntimeError(f"Error condensing thoughts: {e}")
def strip_markdown(markdown_json):
# remove markdown code block
return markdown_json.replace('```json\n', '').replace('```', '').strip()
raise RuntimeError(f'Error condensing thoughts: {e}')
+74 -19
View File
@@ -1,6 +1,9 @@
from typing import List
from . import json
from json import JSONDecodeError
import re
from opendevin.action import (
action_from_dict,
@@ -9,10 +12,10 @@ from opendevin.action import (
from opendevin.observation import (
CmdOutputObservation,
)
from opendevin.exceptions import LLMOutputError
ACTION_PROMPT = """
You're a thoughtful robot. Your main task is this:
%(task)s
Don't expand the scope of your task--just complete it as written.
@@ -87,35 +90,55 @@ You can also use the same action and args from the source monologue.
"""
def get_summarize_monologue_prompt(thoughts):
def get_summarize_monologue_prompt(thoughts: List[dict]):
"""
Gets the prompt for summarizing the monologue
Returns:
- str: A formatted string with the current monologue within the prompt
"""
return MONOLOGUE_SUMMARY_PROMPT % {
'monologue': json.dumps({'old_monologue': thoughts}, indent=2),
}
def get_request_action_prompt(
task: str,
thoughts: List[dict],
background_commands_obs: List[CmdOutputObservation] = [],
task: str,
thoughts: List[dict],
background_commands_obs: List[CmdOutputObservation] = [],
):
"""
Gets the action prompt formatted with appropriate values.
Parameters:
- task (str): The current task the agent is trying to accomplish
- thoughts (List[dict]): The agent's current thoughts
- background_commands_obs (List[CmdOutputObservation]): List of all observed background commands running
Returns:
- str: Formatted prompt string with hint, task, monologue, and background included
"""
hint = ''
if len(thoughts) > 0:
latest_thought = thoughts[-1]
if "action" in latest_thought:
if latest_thought["action"] == 'think':
if latest_thought["args"]['thought'].startswith("OK so my task is"):
if 'action' in latest_thought:
if latest_thought['action'] == 'think':
if latest_thought['args']['thought'].startswith('OK so my task is'):
hint = "You're just getting started! What should you do first?"
else:
hint = "You've been thinking a lot lately. Maybe it's time to take action?"
elif latest_thought["action"] == 'error':
hint = "Looks like that last command failed. Maybe you need to fix it, or try something else."
elif latest_thought['action'] == 'error':
hint = 'Looks like that last command failed. Maybe you need to fix it, or try something else.'
bg_commands_message = ""
bg_commands_message = ''
if len(background_commands_obs) > 0:
bg_commands_message = "The following commands are running in the background:"
bg_commands_message = 'The following commands are running in the background:'
for command_obs in background_commands_obs:
bg_commands_message += f"\n`{command_obs.command_id}`: {command_obs.command}"
bg_commands_message += "\nYou can end any process by sending a `kill` action with the numerical `id` above."
latest_thought = thoughts[-1]
bg_commands_message += (
f'\n`{command_obs.command_id}`: {command_obs.command}'
)
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
return ACTION_PROMPT % {
'task': task,
@@ -124,16 +147,48 @@ def get_request_action_prompt(
'hint': hint,
}
def parse_action_response(response: str) -> Action:
json_start = response.find("{")
json_end = response.rfind("}") + 1
response = response[json_start:json_end]
action_dict = json.loads(response)
"""
Parses a string to find an action within it
Parameters:
- response (str): The string to be parsed
Returns:
- Action: The action that was found in the response string
"""
try:
action_dict = json.loads(response)
except JSONDecodeError:
# Find response-looking json in the output and use the more promising one. Helps with weak llms
response_json_matches = re.finditer(
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
response) # Find all response-looking strings
def rank(match):
return len(match[2]) if match[1] == 'think' else 130 # Crudely rank multiple responses by length
try:
action_dict = json.loads(max(response_json_matches, key=rank)[0]) # Use the highest ranked response
except ValueError as e:
raise LLMOutputError(
"Output from the LLM isn't properly formatted. The model may be misconfigured."
) from e
if 'content' in action_dict:
# The LLM gets confused here. Might as well be robust
action_dict['contents'] = action_dict.pop('content')
return action_from_dict(action_dict)
def parse_summary_response(response: str) -> List[dict]:
"""
Parses a summary of the monologue
Parameters:
- response (str): The response string to be parsed
Returns:
- List[dict]: The list of summaries output by the model
"""
parsed = json.loads(response)
return parsed['new_monologue']
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.agent import Agent
from .agent import PlannerAgent
Agent.register("PlannerAgent", PlannerAgent)
Agent.register('PlannerAgent', PlannerAgent)
+26 -2
View File
@@ -7,20 +7,44 @@ from opendevin.llm.llm import LLM
from opendevin.state import State
from opendevin.action import Action
class PlannerAgent(Agent):
"""
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
"""
def __init__(self, llm: LLM):
"""
Initialize the Planner Agent with an LLM
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
def step(self, state: State) -> Action:
"""
Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
Parameters:
- state (State): The current state given the previous actions and observations
Returns:
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
- Action: The next action to take based on llm response
"""
if state.plan.task.state in ['completed', 'verified', 'abandoned']:
return AgentFinishAction()
prompt = get_prompt(state.plan, state.history)
messages = [{"content": prompt, "role": "user"}]
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = parse_response(action_resp)
return action
def search_memory(self, query: str) -> List[str]:
return []
+66 -41
View File
@@ -1,10 +1,10 @@
import json
from typing import List, Tuple, Dict, Type
from opendevin.controller.agent_controller import print_with_color
from opendevin.plan import Plan
from opendevin.action import Action, action_from_dict
from opendevin.observation import Observation
from opendevin.schema import ActionType
from opendevin.logger import opendevin_logger as logger
from opendevin.action import (
NullAction,
@@ -26,17 +26,17 @@ from opendevin.observation import (
)
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
"run": CmdRunAction,
"kill": CmdKillAction,
"browse": BrowseURLAction,
"read": FileReadAction,
"write": FileWriteAction,
"recall": AgentRecallAction,
"think": AgentThinkAction,
"summarize": AgentSummarizeAction,
"finish": AgentFinishAction,
"add_task": AddTaskAction,
"modify_task": ModifyTaskAction,
ActionType.RUN: CmdRunAction,
ActionType.KILL: CmdKillAction,
ActionType.BROWSE: BrowseURLAction,
ActionType.READ: FileReadAction,
ActionType.WRITE: FileWriteAction,
ActionType.RECALL: AgentRecallAction,
ActionType.THINK: AgentThinkAction,
ActionType.SUMMARIZE: AgentSummarizeAction,
ActionType.FINISH: AgentFinishAction,
ActionType.ADD_TASK: AddTaskAction,
ActionType.MODIFY_TASK: ModifyTaskAction,
}
HISTORY_SIZE = 10
@@ -129,7 +129,20 @@ What is your next thought or action? Again, you must reply with JSON, and only w
%(hint)s
"""
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]):
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
"""
Gets the prompt for the planner agent.
Formatted with the most recent action-observation pairs, current task, and hint based on last action
Parameters:
- plan (Plan): The original plan outlined by the user with LLM defined tasks
- history (List[Tuple[Action, Observation]]): List of corresponding action-observation pairs
Returns:
- str: The formatted string prompt with historical values
"""
plan_str = json.dumps(plan.task.to_dict(), indent=2)
sub_history = history[-HISTORY_SIZE:]
history_dicts = []
@@ -140,12 +153,15 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]):
latest_action = action
if not isinstance(observation, NullObservation):
observation_dict = observation.to_dict()
if "extras" in observation_dict and "screenshot" in observation_dict["extras"]:
del observation_dict["extras"]["screenshot"]
if (
'extras' in observation_dict
and 'screenshot' in observation_dict['extras']
):
del observation_dict['extras']['screenshot']
history_dicts.append(observation_dict)
history_str = json.dumps(history_dicts, indent=2)
hint = ""
hint = ''
current_task = plan.get_current_task()
if current_task is not None:
plan_status = f"You're currently working on this task:\n{current_task.goal}."
@@ -158,30 +174,30 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]):
latest_action_id = latest_action.to_dict()['action']
if current_task is not None:
if latest_action_id == "":
if latest_action_id == '':
hint = "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with."
elif latest_action_id == "run":
hint = "You should think about the command you just ran, what output it gave, and how that affects your plan."
elif latest_action_id == "read":
hint = "You should think about the file you just read, what you learned from it, and how that affects your plan."
elif latest_action_id == "write":
hint = "You just changed a file. You should think about how it affects your plan."
elif latest_action_id == "browse":
hint = "You should think about the page you just visited, and what you learned from it."
elif latest_action_id == "think":
elif latest_action_id == ActionType.RUN:
hint = 'You should think about the command you just ran, what output it gave, and how that affects your plan.'
elif latest_action_id == ActionType.READ:
hint = 'You should think about the file you just read, what you learned from it, and how that affects your plan.'
elif latest_action_id == ActionType.WRITE:
hint = 'You just changed a file. You should think about how it affects your plan.'
elif latest_action_id == ActionType.BROWSE:
hint = 'You should think about the page you just visited, and what you learned from it.'
elif latest_action_id == ActionType.THINK:
hint = "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action."
elif latest_action_id == "recall":
hint = "You should think about the information you just recalled, and how it should affect your plan."
elif latest_action_id == "add_task":
hint = "You should think about the next action to take."
elif latest_action_id == "modify_task":
hint = "You should think about the next action to take."
elif latest_action_id == "summarize":
hint = ""
elif latest_action_id == "finish":
hint = ""
elif latest_action_id == ActionType.RECALL:
hint = 'You should think about the information you just recalled, and how it should affect your plan.'
elif latest_action_id == ActionType.ADD_TASK:
hint = 'You should think about the next action to take.'
elif latest_action_id == ActionType.MODIFY_TASK:
hint = 'You should think about the next action to take.'
elif latest_action_id == ActionType.SUMMARIZE:
hint = ''
elif latest_action_id == ActionType.FINISH:
hint = ''
print_with_color("HINT:\n" + hint, "INFO")
logger.info('HINT:\n' + hint, extra={'msg_type': 'INFO'})
return prompt % {
'task': plan.main_goal,
'plan': plan_str,
@@ -190,9 +206,19 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]):
'plan_status': plan_status,
}
def parse_response(response: str) -> Action:
json_start = response.find("{")
json_end = response.rfind("}") + 1
"""
Parses the model output to find a valid action to take
Parameters:
- response (str): A response from the model that potentially contains an Action.
Returns:
- Action: A valid next action to perform from model output
"""
json_start = response.find('{')
json_end = response.rfind('}') + 1
response = response[json_start:json_end]
action_dict = json.loads(response)
if 'contents' in action_dict:
@@ -200,4 +226,3 @@ def parse_response(response: str) -> Action:
action_dict['content'] = action_dict.pop('contents')
action = action_from_dict(action_dict)
return action
-4
View File
@@ -1,4 +0,0 @@
# This is a template. Run `cp config.toml.template config.toml` to use it.
LLM_API_KEY="<YOUR OPENAI API KEY>"
WORKSPACE_DIR="./workspace"
+54
View File
@@ -0,0 +1,54 @@
FROM node:21.7.2-bookworm-slim as frontend-builder
WORKDIR /app
COPY ./frontend/package.json frontend/package-lock.json ./
RUN npm install
COPY ./frontend ./
RUN npm run make-i18n && npm run build
FROM python:3.12-slim as backend-builder
WORKDIR /app
ENV PYTHONPATH '/app'
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& python3 -m pip install poetry==1.8.2 --break-system-packages
COPY ./pyproject.toml ./poetry.lock ./
RUN touch README.md
RUN poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR
FROM python:3.12-slim as runtime
WORKDIR /app
ENV RUN_AS_DEVIN=false
ENV USE_HOST_NETWORK=false
ENV SSH_HOSTNAME=host.docker.internal
ENV WORKSPACE_BASE=/opt/workspace_base
RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \
&& apt-get install -y curl ssh
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH" \
PYTHONPATH='/app'
COPY --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY ./opendevin ./opendevin
COPY ./agenthub ./agenthub
RUN python opendevin/download.py # No-op to download assets
COPY --from=frontend-builder /app/dist ./frontend/dist
CMD ["uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"]
+2
View File
@@ -0,0 +1,2 @@
DOCKER_REPOSITORY=ghcr.io/opendevin/opendevin
DOCKER_BASE_DIR="."
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail
image_name=$1
push=0
if [[ $2 == "--push" ]]; then
push=1
fi
echo -e "Building: $image_name"
tags=(latest)
if [[ -n $GITHUB_REF_NAME ]]; then
# check if ref name is a version number
if [[ $GITHUB_REF_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
major_version=$(echo $GITHUB_REF_NAME | cut -d. -f1)
minor_version=$(echo $GITHUB_REF_NAME | cut -d. -f1,2)
tags+=($major_version $minor_version)
fi
sanitized=$(echo $GITHUB_REF_NAME | sed 's/[^a-zA-Z0-9.-]\+/-/g')
tags+=($sanitized)
fi
echo "Tags: ${tags[@]}"
dir=./containers/$image_name
if [ ! -f $dir/Dockerfile ]; then
echo "No Dockerfile found"
exit 1
fi
if [ ! -f $dir/config.sh ]; then
echo "No config.sh found for Dockerfile"
exit 1
fi
source $dir/config.sh
echo "Repo: $DOCKER_REPOSITORY"
echo "Base dir: $DOCKER_BASE_DIR"
#docker pull $DOCKER_REPOSITORY:main || true # try to get any cached layers
args=""
for tag in ${tags[@]}; do
args+=" -t $DOCKER_REPOSITORY:$tag"
done
if [[ $push -eq 1 ]]; then
args+=" --push"
fi
docker buildx build \
$args \
--platform linux/amd64,linux/arm64 \
-f $dir/Dockerfile $DOCKER_BASE_DIR
@@ -29,6 +29,10 @@ RUN conda --version
COPY environment.yml .
RUN conda env create -f environment.yml
# Add commands
COPY ./commands.sh .
RUN . ./commands.sh
# Some missing packages
RUN pip install datasets python-dotenv gitpython
+2
View File
@@ -0,0 +1,2 @@
DOCKER_REPOSITORY=ghcr.io/opendevin/eval-swe-bench
DOCKER_BASE_DIR=evaluation/SWE-bench
@@ -14,4 +14,8 @@ RUN apt-get update && apt-get install -y \
python3-venv \
python3-dev \
build-essential \
openssh-server \
sudo \
&& rm -rf /var/lib/apt/lists/*
RUN service ssh start
+2
View File
@@ -0,0 +1,2 @@
DOCKER_REPOSITORY=ghcr.io/opendevin/sandbox
DOCKER_BASE_DIR="."
+10 -5
View File
@@ -7,7 +7,6 @@ repos:
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- id: requirements-txt-fixer
- repo: https://github.com/hhatto/autopep8
rev: v2.1.0
@@ -22,18 +21,24 @@ repos:
pass_filenames: false
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.3
# Ruff version.
rev: v0.3.7
hooks:
# Run the linter.
- id: ruff
entry: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
always_run: true
pass_filenames: false
types_or: [ python, pyi, jupyter ]
args: [ --fix ]
# Run the formatter.
- id: ruff-format
entry: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
types_or: [ python, pyi, jupyter ]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
additional_dependencies: [types-requests, types-setuptools, types-pyyaml, types-toml]
entry: mypy --config-file dev_config/python/mypy.ini opendevin/ agenthub/
always_run: true
pass_filenames: false
+1 -1
View File
@@ -17,7 +17,7 @@ The generation of the backend architecture diagram is partially automated. The d
4. Review the diff between the new and the previous diagram and manually check if the changes are correct.
*Make sure not to remove parts that were manually added to the diagram in the past and are still relevant.*
4. Add the commit hash of the commit that was used to generat the diagram to the diagram footer.
4. Add the commit hash of the commit that was used to generate the diagram to the diagram footer.
5. Export the diagram as PNG and SVG files and replace the existing diagrams in the `docs/architecture` directory. This can be done with (e.g. [PlantText](https://www.planttext.com/))
+31
View File
@@ -0,0 +1,31 @@
# Azure OpenAI LLM Guide
# 1. Completion
OpenDevin uses LiteLLM for completion calls. You can find their documentation on Azure [here](https://docs.litellm.ai/docs/providers/azure)
## azure openai configs
When running the OpenDevin Docker image, you'll need to set the following environment variables using `-e`:
```
LLM_BASE_URL="<azure-api-base-url>" # e.g. "https://openai-gpt-4-test-v-1.openai.azure.com/"
LLM_API_KEY="<azure-api-key>"
LLM_MODEL="azure/<your-gpt-deployment-name>"
AZURE_API_VERSION = "<api-version>" # e.g. "2024-02-15-preview"
```
# 2. Embeddings
OpenDevin uses llama-index for embeddings. You can find their documentation on Azure [here](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/)
## azure openai configs
The model used for Azure OpenAI embeddings is "text-embedding-ada-002".
You need the correct deployment name for this model in your Azure account.
When running OpenDevin in Docker, set the following environment variables using `-e`:
```
LLM_EMBEDDING_MODEL="azureopenai"
DEPLOYMENT_NAME = "<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
LLM_API_VERSION = "<api-version>" # e.g. "2024-02-15-preview"
```
+96
View File
@@ -0,0 +1,96 @@
# Agents and Capabilities
## Monologue Agent:
### Description:
The Monologue Agent utilizes long and short term memory to complete tasks.
Long term memory is stored as a LongTermMemory object and the model uses it to search for examples from the past.
Short term memory is stored as a Monologue object and the model can condense it as necessary.
### Actions:
`Action`,
`NullAction`,
`CmdRunAction`,
`FileWriteAction`,
`FileReadAction`,
`AgentRecallAction`,
`BrowseURLAction`,
`AgentThinkAction`
### Observations:
`Observation`,
`NullObservation`,
`CmdOutputObservation`,
`FileReadObservation`,
`AgentRecallObservation`,
`BrowserOutputObservation`
### Methods:
`__init__`: Initializes the agent with a long term memory, and an internal monologue
`_add_event`: Appends events to the monologue of the agent and condenses with summary automatically if the monologue is too long
`_initialize`: Utilizes the `INITIAL_THOUGHTS` list to give the agent a context for its capabilities and how to navigate the `/workspace`
`step`: Modifies the current state by adding the most rescent actions and observations, then prompts the model to think about its next action to take.
`search_memory`: Uses `VectorIndexRetriever` to find related memories within the long term memory.
## Planner Agent:
### Description:
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
### Actions:
`NullAction`,
`CmdRunAction`,
`CmdKillAction`,
`BrowseURLAction`,
`FileReadAction`,
`FileWriteAction`,
`AgentRecallAction`,
`AgentThinkAction`,
`AgentFinishAction`,
`AgentSummarizeAction`,
`AddTaskAction`,
`ModifyTaskAction`,
### Observations:
`Observation`,
`NullObservation`,
`CmdOutputObservation`,
`FileReadObservation`,
`AgentRecallObservation`,
`BrowserOutputObservation`
### Methods:
`__init__`: Initializes an agent with `llm`
`step`: Checks to see if current step is completed, returns `AgentFinishAction` if True. Otherwise, creates a plan prompt and sends to model for inference, adding the result as the next action.
`search_memory`: Not yet implemented
## CodeAct Agent:
### Description:
The Code Act Agent is a minimalist agent. The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
### Actions:
`Action`,
`CmdRunAction`,
`AgentEchoAction`,
`AgentFinishAction`,
### Observations:
`CmdOutputObservation`,
`AgentMessageObservation`,
### Methods:
`__init__`: Initializes an agent with `llm` and a list of messages `List[Mapping[str, str]]`
`step`: First, gets messages from state and then compiles them into a list for context. Next, pass the context list with the prompt to get the next command to execute. Finally, Execute command if valid, else return `AgentEchoAction(INVALID_INPUT_MESSAGE)`
`search_memory`: Not yet implemented
@@ -7,7 +7,7 @@ Linux:
```
curl -fsSL https://ollama.com/install.sh | sh
```
Windows or macOS:
Windows or macOS:
- Download from [here](https://ollama.com/download/)
@@ -60,30 +60,10 @@ sudo systemctl stop ollama
For more info go [here](https://github.com/ollama/ollama/blob/main/docs/faq.md)
## 3. Follow the default installation of OpenDevin:
```
git clone git@github.com:OpenDevin/OpenDevin.git
```
or
```
git clone git@github.com:<YOUR-USERNAME>/OpenDevin.git
```
## 3. Start OpenDevin
then
```
cd OpenDevin
```
## 4. Run setup commands:
```
make build
make setup-config
```
## 5. Modify config file:
- After running `make setup-config` you will see a generated file `OpenDevin/config.toml`.
- Open this file and modify it to your needs based on this template:
Use the instructions in [README.md](/README.md) to start OpenDevin using Docker.
When running `docker run`, add the following environment variables using `-e`:
```
LLM_API_KEY="ollama"
@@ -92,20 +72,37 @@ LLM_EMBEDDING_MODEL="local"
LLM_BASE_URL="http://localhost:<port_number>"
WORKSPACE_DIR="./workspace"
```
Notes:
- The API key should be set to `"ollama"`
- The base url needs to be `localhost`
Notes:
- The API key should be set to `"ollama"`
- The base url needs to be `localhost`
- By default ollama port is `11434` unless you set it
- `model_name` needs to be the entire model name
- Example: `LLM_MODEL="ollama/llama2:13b-chat-q4_K_M"`
## 6. Start OpenDevin:
You should now be able to connect to `http://localhost:3001/` with your local model running!
At this point everything should be set up and working properly.
1. Start by running the ollama server using the method outlined above
2. Run `make build` in your terminal `~/OpenDevin/`
3. Run `make run` in your terminal
4. If that fails try running the server and front end in sepparate terminals:
- In the first terminal `make start-backend`
- In the second terminal `make start-frontend`
5. you should now be able to connect to `http://localhost:3001/` with your local model running!
## Additional Notes for WSL2 Users:
1. If you encounter the following error during setup: `Exception: Failed to create opendevin user in sandbox: b'useradd: UID 0 is not unique\n'`
You can resolve it by running:
```
export SANDBOX_USER_ID=1000
```
2. If you face issues running Poetry even after installing it during the build process, you may need to add its binary path to your environment:
```
export PATH="$HOME/.local/bin:$PATH"
```
3. If you experiencing issues related to networking, such as `NoneType object has no attribute 'request'` when executing `make run`, you may need to configure your WSL2 networking settings. Follow these steps:
- Open or create the `.wslconfig` file located at `C:\Users\%username%\.wslconfig` on your Windows host machine.
- Add the following configuration to the `.wslconfig` file:
```
[wsl2]
networkingMode=mirrored
localhostForwarding=true
```
- Save the `.wslconfig` file.
- Restart WSL2 completely by exiting any running WSL2 instances and executing the command `wsl --shutdown` in your command prompt or terminal.
- After restarting WSL, attempt to execute `make run` again. The networking issue should be resolved.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+49 -2
View File
@@ -17,16 +17,63 @@ all the preprocessing/evaluation/analysis scripts.
- Ensure compatibility with OpenAI interface for inference.
- Serve open source models, prioritizing high concurrency and throughput.
## Tasks
### SWE-bench
## SWE-bench
- notebooks
- `devin_eval_analysis.ipynb`: notebook analyzing devin's outputs
- scripts
- `prepare_devin_outputs_for_evaluation.py`: script fetching and converting [devin's output](https://github.com/CognitionAI/devin-swebench-results/tree/main) into the desired json file for evaluation.
- usage: `python prepare_devin_outputs_for_evaluation.py <setting>` where setting can be `passed`, `failed` or `all`
- resources
- Devin related SWE-bench test subsets
- [🤗 OpenDevin/SWE-bench-devin-passed](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-passed)
- [🤗 OpenDevin/SWE-bench-devin-full-filtered](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-full-filtered)
- Devin's outputs processed for evaluations is available on [Huggingface](https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output)
- get predictions that passed the test: `wget https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output/raw/main/devin_swe_passed.json`
- get all predictions `wget https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output/raw/main/devin_swe_outputs.json`
See [`SWE-bench/README.md`](./SWE-bench/README.md) for more details on how to run SWE-Bench for evaluation.
### Results
We have refined the original SWE-bench evaluation pipeline to enhance its efficiency and reliability. The updates are as follows:
- Reuse testbeds and Conda environments.
- Additionally try `patch` command for patch application if `git apply` command fails.
#### Results on SWE-bench-devin-passed
[🤗 OpenDevin/SWE-bench-devin-passed](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-passed)
| Model/Agent | #instances | #init | #apply | #resolve |
|------------------------|------------|-------|--------|----------|
| Gold | 79 | 79 | 79 | 79 |
| Devin | 79 | 79 | 76 | 76 |
#init: number of instances where testbeds have been successfully initialized.
In the 3 Devin-failed instances (see below), Devin has made changes to the tests, which are incompatible with the provided test patch and causes failures during patch application. The evaluation adopted by Devin does not seem to align with the original SWE-bench evaluation.
```shell
django__django-11244
scikit-learn__scikit-learn-10870
sphinx-doc__sphinx-9367
```
#### Results on SWE-bench-devin-failed
| Model/Agent | #instances | #init | #apply | #resolve |
|------------------------|------------|-------|--------|----------|
| Gold | 491 | 491 | 491 | 371 |
| Devin | 491 | 491 | 463 | 7 |
Devin **passes** 7 instances on the `SWE-bench-devin-failed` subset. SWE-bench dataset appears to be noisy, evidenced by 120 instances where gold patches do not pass.
We have filtered out the problematic 120 instances, resulting in the creation of the `SWE-bench-devin-full-filtered` subset.
## Results on SWE-bench-devin-full-filtered
[🤗 OpenDevin/SWE-bench-devin-full-filtered](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-full-filtered)
| Model/Agent | #instances | #init | #apply | #resolve |
|------------------------|------------|-------|--------|----------|
| Gold | 450 | 450 | 450 | 450 |
| Devin | 450 | 450 | 426 | 83 |
-31
View File
@@ -1,31 +0,0 @@
DOCKER_BUILD_REGISTRY=ghcr.io
DOCKER_BUILD_ORG=opendevin
DOCKER_BUILD_REPO=eval-swe-bench
DOCKER_BUILD_TAG=v0.1.0
FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(DOCKER_BUILD_TAG)
LATEST_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):latest
MAJOR_VERSION=$(shell echo $(DOCKER_BUILD_TAG) | cut -d. -f1)
MAJOR_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(MAJOR_VERSION)
MINOR_VERSION=$(shell echo $(DOCKER_BUILD_TAG) | cut -d. -f1,2)
MINOR_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(MINOR_VERSION)
# normally, for local build testing or development. use cross platform build for sharing images to others.
build:
docker build -f Dockerfile -t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} .
push:
docker push ${FULL_IMAGE} ${LATEST_FULL_IMAGE}
test:
docker buildx build --platform linux/amd64 \
-t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} --load -f Dockerfile .
# cross platform build, you may need to manually stop the buildx(buildkit) container
all:
docker buildx build --platform linux/amd64,linux/arm64 \
-t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} -t ${MINOR_FULL_IMAGE} --push -f Dockerfile .
get-full-image:
@echo ${FULL_IMAGE}
+155
View File
@@ -0,0 +1,155 @@
# @yaml
# signature: search_dir <search_term> [<dir>]
# docstring: searches for search_term in all files in dir. If dir is not provided, searches in the current directory
# arguments:
# search_term:
# type: string
# description: the term to search for
# required: true
# dir:
# type: string
# description: the directory to search in (if not provided, searches in the current directory)
# required: false
search_dir() {
if [ $# -eq 1 ]; then
local search_term="$1"
local dir="./"
elif [ $# -eq 2 ]; then
local search_term="$1"
if [ -d "$2" ]; then
local dir="$2"
else
echo "Directory $2 not found"
return
fi
else
echo "Usage: search_dir <search_term> [<dir>]"
return
fi
dir=$(realpath "$dir")
local matches=$(find "$dir" -type f ! -path '*/.*' -exec grep -nIH "$search_term" {} + | cut -d: -f1 | sort | uniq -c)
# if no matches, return
if [ -z "$matches" ]; then
echo "No matches found for \"$search_term\" in $dir"
return
fi
# Calculate total number of matches
local num_matches=$(echo "$matches" | awk '{sum+=$1} END {print sum}')
# calculate total number of files matched
local num_files=$(echo "$matches" | wc -l | awk '{$1=$1; print $0}')
# if num_files is > 100, print an error
if [ $num_files -gt 100 ]; then
echo "More than $num_files files matched for \"$search_term\" in $dir. Please narrow your search."
return
fi
echo "Found $num_matches matches for \"$search_term\" in $dir:"
echo "$matches" | awk '{$2=$2; gsub(/^\.+\/+/, "./", $2); print $2 " ("$1" matches)"}'
echo "End of matches for \"$search_term\" in $dir"
}
# @yaml
# signature: search_file <search_term> [<file>]
# docstring: searches for search_term in file. If file is not provided, searches in the current open file
# arguments:
# search_term:
# type: string
# description: the term to search for
# required: true
# file:
# type: string
# description: the file to search in (if not provided, searches in the current open file)
# required: false
search_file() {
# Check if the first argument is provided
if [ -z "$1" ]; then
echo "Usage: search_file <search_term> [<file>]"
return
fi
# Check if the second argument is provided
if [ -n "$2" ]; then
# Check if the provided argument is a valid file
if [ -f "$2" ]; then
local file="$2" # Set file if valid
else
echo "Usage: search_file <search_term> [<file>]"
echo "Error: File name $2 not found. Please provide a valid file name."
return # Exit if the file is not valid
fi
else
# Check if a file is open
if [ -z "$CURRENT_FILE" ]; then
echo "No file open. Use the open command first."
return # Exit if no file is open
fi
local file="$CURRENT_FILE" # Set file to the current open file
fi
local search_term="$1"
file=$(realpath "$file")
# Use grep to directly get the desired formatted output
local matches=$(grep -nH "$search_term" "$file")
# Check if no matches were found
if [ -z "$matches" ]; then
echo "No matches found for \"$search_term\" in $file"
return
fi
# Calculate total number of matches
local num_matches=$(echo "$matches" | wc -l | awk '{$1=$1; print $0}')
# calculate total number of lines matched
local num_lines=$(echo "$matches" | cut -d: -f1 | sort | uniq | wc -l | awk '{$1=$1; print $0}')
# if num_lines is > 100, print an error
if [ $num_lines -gt 100 ]; then
echo "More than $num_lines lines matched for \"$search_term\" in $file. Please narrow your search."
return
fi
# Print the total number of matches and the matches themselves
echo "Found $num_matches matches for \"$search_term\" in $file:"
echo "$matches" | cut -d: -f1-2 | sort -u -t: -k2,2n | while IFS=: read -r filename line_number; do
echo "Line $line_number:$(sed -n "${line_number}p" "$file")"
done
echo "End of matches for \"$search_term\" in $file"
}
# @yaml
# signature: find_file <file_name> [<dir>]
# docstring: finds all files with the given name in dir. If dir is not provided, searches in the current directory
# arguments:
# file_name:
# type: string
# description: the name of the file to search for
# required: true
# dir:
# type: string
# description: the directory to search in (if not provided, searches in the current directory)
# required: false
find_file() {
if [ $# -eq 1 ]; then
local file_name="$1"
local dir="./"
elif [ $# -eq 2 ]; then
local file_name="$1"
if [ -d "$2" ]; then
local dir="$2"
else
echo "Directory $2 not found"
return
fi
else
echo "Usage: find_file <file_name> [<dir>]"
return
fi
dir=$(realpath "$dir")
local matches=$(find "$dir" -type f -name "$file_name")
# if no matches, return
if [ -z "$matches" ]; then
echo "No matches found for \"$file_name\" in $dir"
return
fi
# Calculate total number of matches
local num_matches=$(echo "$matches" | wc -l | awk '{$1=$1; print $0}')
echo "Found $num_matches matches for \"$file_name\" in $dir:"
echo "$matches" | awk '{print $0}'
}
+3 -3
View File
@@ -14,9 +14,9 @@ To run the tests for OpenDevin project, you can use the provided test runner scr
3. Navigate to the root directory of the project.
4. Run the test suite using the test runner script with the required arguments:
```
python evaluation/regression/run_tests.py --OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx --model=gpt-4-0125-preview
python evaluation/regression/run_tests.py --OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx --model=gpt-3.5-turbo-1106
```
Replace `sk-xxxxxxxxxxxxxxxxxxxxxx` with your actual OpenAI API key. The default model is `gpt-4-0125-preview`, but you can specify a different model if needed.
Replace `sk-xxxxxxxxxxxxxxxxxxxxxx` with your actual OpenAI API key. The default model is `gpt-3.5-turbo-1106`, but you can specify a different model if needed.
The test runner will discover and execute all the test cases in the `cases/` directory, and display the results of the test suite, including the status of each individual test case and the overall summary.
@@ -76,4 +76,4 @@ The test cases can be customized by modifying the fixtures defined in the `conft
You can modify these fixtures to change the behavior of the test cases or add new ones as needed.
If you have any questions or need further assistance, feel free to reach out to the project maintainers.
If you have any questions or need further assistance, feel free to reach out to the project maintainers.
+5 -5
View File
@@ -67,9 +67,9 @@ def model(request):
request: The pytest request object.
Returns:
The model name, defaulting to "gpt-4-0125-preview".
"""
return request.config.getoption("model", default="gpt-4-0125-preview")
The model name, defaulting to "gpt-3.5-turbo-1106".
""
return request.config.getoption("model", default="gpt-3.5-turbo-1106")
@pytest.fixture
def run_test_case(test_cases_dir, workspace_dir, request):
@@ -115,7 +115,7 @@ def run_test_case(test_cases_dir, workspace_dir, request):
"monologue_agent":"MonologueAgent",
"codeact_agent":"CodeActAgent"
}
process = subprocess.Popen(["python3", f"{SCRIPT_DIR}/../../opendevin/main.py", "-d", f"{os.path.join(agent_dir, 'workspace')}", "-c", f"{agents_ref[agent]}", "-t", f"{task}", "-m", "gpt-4-0125-preview"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
process = subprocess.Popen(["python3", f"{SCRIPT_DIR}/../../opendevin/main.py", "-d", f"{os.path.join(agent_dir, 'workspace')}", "-c", f"{agents_ref[agent]}", "-t", f"{task}", "-m", "gpt-3.5-turbo-1106"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
stdout, stderr = process.communicate()
logging.info(f"Stdout: {stdout}")
logging.error(f"Stderr: {stderr}")
@@ -139,4 +139,4 @@ def pytest_configure(config):
logging.FileHandler(f"test_results_{now.strftime('%Y%m%d_%H%M%S')}.log"),
logging.StreamHandler()
]
)
)
+2995 -2423
View File
File diff suppressed because it is too large Load Diff
+19 -12
View File
@@ -11,28 +11,25 @@
"@nextui-org/react": "^2.2.10",
"@react-types/shared": "^3.22.1",
"@reduxjs/toolkit": "^2.2.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.0.0 ",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@vitejs/plugin-react": "^4.2.1",
"@xterm/xterm": "^5.4.0",
"clsx": "^2.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^11.0.24",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.1",
"i18next-http-backend": "^2.5.0",
"jose": "^5.2.3",
"monaco-editor": "^0.47.0",
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.0",
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.2.2",
"typescript": "^5.4.3",
"vite": "^5.1.6",
"vite-tsconfig-paths": "^4.3.2",
"web-vitals": "^2.1.4",
@@ -44,7 +41,7 @@
"test": "jest",
"preview": "vite preview",
"make-i18n": "node scripts/make-i18n-translations.cjs",
"prelint": "pnpm run make-i18n",
"prelint": "npm run make-i18n",
"lint": "eslint src/**/*.ts* && prettier --check src/**/*.ts*",
"prepare": "cd .. && husky install frontend/.husky"
},
@@ -67,11 +64,20 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^29.5.12",
"@types/node": "^18.0.0 ",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
@@ -85,9 +91,10 @@
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.2",
"ts-jest": "^29.1.2"
"ts-jest": "^29.1.2",
"typescript": "^5.4.3"
},
"packageManager": "pnpm@8.15.6",
"packageManager": "npm@10.5.0",
"volta": {
"node": "18.20.1"
}
-8596
View File
File diff suppressed because it is too large Load Diff
-9
View File
@@ -1,9 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+80 -16
View File
@@ -1,11 +1,19 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import "./App.css";
import { Toaster } from "react-hot-toast";
import CogTooth from "./assets/cog-tooth";
import ChatInterface from "./components/ChatInterface";
import Errors from "./components/Errors";
import LoadMessageModal from "./components/LoadMessageModal";
import { Container, Orientation } from "./components/Resizable";
import SettingModal from "./components/SettingModal";
import Terminal from "./components/Terminal";
import Workspace from "./components/Workspace";
import { fetchMsgTotal } from "./services/session";
import { fetchConfigurations, saveSettings } from "./services/settingsService";
import Socket from "./services/socket";
import { ResConfigurations, ResFetchMsgTotal } from "./types/ResponseType";
import { getCachedConfig } from "./utils/storage";
interface Props {
setSettingOpen: (isOpen: boolean) => void;
@@ -13,7 +21,7 @@ interface Props {
function LeftNav({ setSettingOpen }: Props): JSX.Element {
return (
<div className="flex flex-col h-full p-4 bg-bg-dark w-16 items-center shrink-0">
<div className="flex flex-col h-full p-4 bg-neutral-900 w-16 items-center shrink-0">
<div
className="mt-auto cursor-pointer hover:opacity-80"
onClick={() => setSettingOpen(true)}
@@ -24,31 +32,87 @@ function LeftNav({ setSettingOpen }: Props): JSX.Element {
);
}
// React.StrictMode will cause double rendering, use this to prevent it
let initOnce = false;
function App(): JSX.Element {
const [settingOpen, setSettingOpen] = useState(false);
const [loadMsgWarning, setLoadMsgWarning] = useState(false);
const getConfigurations = () => {
fetchConfigurations()
.then((data: ResConfigurations) => {
const settings = getCachedConfig();
saveSettings(
Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, String(value)]),
),
settings,
true,
);
})
.catch();
};
const getMsgTotal = () => {
fetchMsgTotal()
.then((data: ResFetchMsgTotal) => {
if (data.msg_total > 0) {
setLoadMsgWarning(true);
}
})
.catch();
};
useEffect(() => {
if (initOnce) return;
initOnce = true;
Socket.registerCallback("open", [getConfigurations, getMsgTotal]);
getConfigurations();
getMsgTotal();
}, []);
const handleCloseModal = () => {
setSettingOpen(false);
};
return (
<div className="flex h-screen bg-bg-dark text-white">
<LeftNav setSettingOpen={setSettingOpen} />
<div className="flex flex-col grow gap-3 py-3 pr-3">
<div className="flex gap-3 grow">
<div className="w-[500px] shrink-0 rounded-xl overflow-hidden border border-border">
<ChatInterface />
</div>
<div className="flex flex-col flex-1 overflow-hidden rounded-xl bg-bg-workspace border border-border">
<Workspace />
</div>
</div>
<div className="h-72 shrink-0 bg-bg-workspace rounded-xl border border-border flex flex-col">
<Terminal key="terminal" />
</div>
<div className="h-screen w-screen flex flex-col">
<div className="flex grow bg-neutral-900 text-white min-h-0">
<LeftNav setSettingOpen={setSettingOpen} />
<Container
orientation={Orientation.VERTICAL}
className="grow p-3 py-3 pr-3 min-w-0"
initialSize={window.innerHeight - 300}
firstChild={
<Container
orientation={Orientation.HORIZONTAL}
className="grow h-full min-h-0 min-w-0"
initialSize={500}
firstChild={<ChatInterface />}
firstClassName="min-w-[500px] rounded-xl overflow-hidden border border-neutral-600"
secondChild={<Workspace />}
secondClassName="flex flex-col overflow-hidden rounded-xl bg-neutral-800 border border-neutral-600 grow min-w-[500px] min-w-[500px]"
/>
}
firstClassName="min-h-72"
secondChild={<Terminal key="terminal" />}
secondClassName="min-h-72 bg-neutral-800 rounded-xl border border-neutral-600 flex flex-col"
/>
</div>
{/* This div is for the footer that will be added later
<div className="h-8 w-full border-t border-border px-2" />
*/}
<SettingModal isOpen={settingOpen} onClose={handleCloseModal} />
<LoadMessageModal
isOpen={loadMsgWarning}
onClose={() => setLoadMsgWarning(false)}
/>
<Errors />
<Toaster />
</div>
);
}
@@ -0,0 +1,24 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "../i18n/declaration";
function AgentStatusBar() {
const { t } = useTranslation();
// TODO: Extend the agent status, e.g.:
// - Agent is typing
// - Agent is initializing
// - Agent is thinking
// - Agent is ready
// - Agent is not available
return (
<div className="flex items-center space-x-3 ml-6">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm text-stone-400">
{t(I18nKey.CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE)}
</span>
</div>
);
}
export default AgentStatusBar;
+1 -1
View File
@@ -13,7 +13,7 @@ function Browser(): JSX.Element {
: `data:image/png;base64,${screenshotSrc || ""}`;
return (
<div className="h-full m-2 bg-bg-workspace mockup-browser">
<div className="h-full m-2 bg-neutral-700 mockup-browser">
<div className="mockup-browser-toolbar">
<div className="input">{url}</div>
</div>
+38 -106
View File
@@ -1,19 +1,16 @@
import { Card, CardBody } from "@nextui-org/react";
import React, { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { IoMdChatbubbles } from "react-icons/io";
import { useSelector } from "react-redux";
import assistantAvatar from "../assets/assistant-avatar.png";
import userAvatar from "../assets/user-avatar.png";
import { useTypingEffect } from "../hooks/useTypingEffect";
import { I18nKey } from "../i18n/declaration";
import {
addAssistantMessageToChat,
setCurrentQueueMarkerState,
setCurrentTypingMsgState,
setTypingAcitve,
setTypingActive,
takeOneAndType,
} from "../services/chatService";
import { Message } from "../state/chatSlice";
import { RootState } from "../store";
import AgentStatusBar from "./AgentStatusBar";
import Input from "./Input";
interface IChatBubbleProps {
@@ -29,40 +26,35 @@ interface IChatBubbleProps {
*
*/
function TypingChat() {
const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
useSelector((state: RootState) => state.chat);
const { typeThis } = useSelector((state: RootState) => state.chat);
const messageContent = useTypingEffect([currentTypingMessage], {
const messageContent = useTypingEffect([typeThis?.content], {
loop: false,
setTypingAcitve,
setCurrentQueueMarkerState,
currentQueueMarker,
playbackRate: 0.1,
setTypingActive,
playbackRate: 0.099,
addAssistantMessageToChat,
assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
takeOneAndType,
typeThis,
});
return (
currentQueueMarker !== null && (
<Card className="bg-success-100">
<CardBody>{messageContent}</CardBody>
</Card>
)
<Card className="bg-neutral-500">
<CardBody>{messageContent}</CardBody>
</Card>
);
}
function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
return (
<div className="flex mb-2.5 pr-5 pl-5">
<div
className={`flex mb-2.5 pr-5 pl-5 max-w-[90%] ${msg?.sender === "user" ? "self-end" : ""}`}
>
<div
className={`flex mt-2.5 mb-0 min-w-0 ${msg?.sender === "user" && "flex-row-reverse ml-auto"}`}
>
<img
src={msg?.sender === "user" ? userAvatar : assistantAvatar}
alt={`${msg?.sender} avatar`}
className="w-[40px] h-[40px] mx-2.5"
/>
<Card className={`${msg?.sender === "user" ? "bg-primary-100" : ""}`}>
<Card
className={`${msg?.sender === "user" ? "bg-neutral-700" : "bg-neutral-500"}`}
>
<CardBody>{msg?.content}</CardBody>
</Card>
</div>
@@ -72,14 +64,9 @@ function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
function MessageList(): JSX.Element {
const messagesEndRef = useRef<HTMLDivElement>(null);
const {
messages,
queuedTyping,
typingActive,
currentQueueMarker,
currentTypingMessage,
newChatSequence,
} = useSelector((state: RootState) => state.chat);
const { typingActive, newChatSequence, typeThis } = useSelector(
(state: RootState) => state.chat,
);
const messageScroll = () => {
messagesEndRef.current?.scrollIntoView({
@@ -101,65 +88,21 @@ function MessageList(): JSX.Element {
}, [newChatSequence, typingActive]);
useEffect(() => {
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
if (typeThis.content === "") return;
if (
currentQueueMarker !== null &&
currentQueueMarker !== 0 &&
currentTypingMessage !== newMessage
) {
setCurrentTypingMsgState(
messages?.[queuedTyping?.[currentQueueMarker]]?.content,
);
}
}, [queuedTyping]);
useEffect(() => {
if (currentTypingMessage === "") return;
if (!typingActive) setTypingAcitve(true);
}, [currentTypingMessage]);
useEffect(() => {
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
if (
newMessage &&
typingActive === false &&
currentTypingMessage !== newMessage
) {
if (currentQueueMarker !== 0) {
setCurrentTypingMsgState(
messages?.[queuedTyping?.[currentQueueMarker]]?.content,
);
}
}
}, [typingActive]);
useEffect(() => {
if (currentQueueMarker === 0) {
setCurrentTypingMsgState(messages?.[queuedTyping?.[0]]?.content);
}
}, [currentQueueMarker]);
if (!typingActive) setTypingActive(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typeThis]);
return (
<div className="flex-1 overflow-y-auto">
{newChatSequence.map((msg, index) =>
// eslint-disable-next-line no-nested-ternary
msg.sender === "user" || msg.sender === "assistant" ? (
<ChatBubble key={index} msg={msg} />
) : (
<div key={index} />
),
)}
<div className="flex-1 overflow-y-auto flex flex-col">
{newChatSequence.map((msg, index) => (
<ChatBubble key={index} msg={msg} />
))}
{typingActive && (
<div className="flex mb-2.5 pr-5 pl-5 bg-s">
<div className="flex mb-2.5 pr-5 pl-5 max-w-[90%]">
<div className="flex mt-2.5 mb-0 min-w-0 ">
<img
src={assistantAvatar}
alt="assistant avatar"
className="w-[40px] h-[40px] mx-2.5"
/>
<TypingChat />
</div>
</div>
@@ -169,28 +112,17 @@ function MessageList(): JSX.Element {
);
}
function InitializingStatus(): JSX.Element {
const { t } = useTranslation();
return (
<div className="flex items-center m-auto h-full">
<img
src={assistantAvatar}
alt="assistant avatar"
className="w-[40px] h-[40px] mx-2.5"
/>
<div>{t(I18nKey.CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE)}</div>
</div>
);
}
function ChatInterface(): JSX.Element {
const { initialized } = useSelector((state: RootState) => state.task);
return (
<div className="flex flex-col h-full p-0 bg-bg-workspace">
<div className="border-b border-border text-lg px-4 py-2">Chat</div>
{initialized ? <MessageList /> : <InitializingStatus />}
<div className="flex flex-col h-full p-0 bg-neutral-800">
<div className="flex items-center gap-2 border-b border-neutral-600 text-sm px-4 py-2">
<IoMdChatbubbles />
Chat
</div>
<MessageList />
{initialized ? null : <AgentStatusBar />}
<Input />
</div>
);
+46 -17
View File
@@ -1,44 +1,73 @@
import React from "react";
import Editor, { Monaco } from "@monaco-editor/react";
import { useSelector } from "react-redux";
import { Tab, Tabs } from "@nextui-org/react";
import type { editor } from "monaco-editor";
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "../store";
import Files from "./Files";
function CodeEditor(): JSX.Element {
const [selectedFileName, setSelectedFileName] = useState("welcome");
const [explorerOpen, setExplorerOpen] = useState(true);
const code = useSelector((state: RootState) => state.code.code);
const bgColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-workspace")
.trim();
const handleEditorDidMount = (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => {
// 定义一个自定义主题
// 定义一个自定义主题 - English: Define a custom theme
monaco.editor.defineTheme("my-theme", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": bgColor,
"editor.background": "#171717",
},
});
// 应用自定义主题
// 应用自定义主题 - English: apply custom theme
monaco.editor.setTheme("my-theme");
};
return (
<div className="w-full h-full bg-bg-workspace">
<Editor
height="95%"
theme="vs-dark"
defaultLanguage="python"
defaultValue="# Welcome to OpenDevin!"
value={code}
onMount={handleEditorDidMount}
<div className="flex h-full w-full bg-neutral-900 transition-all duration-500 ease-in-out">
<Files
setSelectedFileName={setSelectedFileName}
setExplorerOpen={setExplorerOpen}
explorerOpen={explorerOpen}
/>
<div className="flex flex-col min-h-0 w-full">
<Tabs
disableCursorAnimation
classNames={{
base: "border-b border-divider",
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 border-divider",
cursor: "w-full bg-neutral-600 rounded-none",
tab: "max-w-fit px-4 h-[36px]",
tabContent: "group-data-[selected=true]:text-white ",
}}
aria-label="Options"
>
<Tab
key={
selectedFileName === ""
? "Welcome"
: selectedFileName.toLocaleLowerCase()
}
title={!selectedFileName ? "Welcome" : selectedFileName}
/>
</Tabs>
<div className="flex grow">
<Editor
height="100%"
defaultLanguage="python"
defaultValue="# Welcome to OpenDevin!"
value={code}
onMount={handleEditorDidMount}
/>
</div>
</div>
</div>
);
}
+2 -3
View File
@@ -1,15 +1,14 @@
import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "../store";
import "./css/Errors.css";
function Errors(): JSX.Element {
const errors = useSelector((state: RootState) => state.errors.errors);
return (
<div className="errors">
<div className="fixed left-1/2 transform -translate-x-1/2 top-4 z-50">
{errors.map((error, index) => (
<div key={index} className="error">
<div key={index} className="bg-red-800 p-4 rounded-md shadow-md mb-2">
ERROR: {error}
</div>
))}
+41
View File
@@ -0,0 +1,41 @@
import React from "react";
import { DiJavascript } from "react-icons/di";
import {
FaCss3,
FaFile,
FaHtml5,
FaList,
FaMarkdown,
FaNpm,
FaPython,
} from "react-icons/fa";
interface FileIconProps {
filename: string;
}
function FileIcon({ filename }: FileIconProps): JSX.Element | null {
const extension = filename.slice(filename.lastIndexOf(".") + 1);
switch (extension) {
case "js":
return <DiJavascript />;
case "ts":
return <DiJavascript />;
case "py":
return <FaPython />;
case "css":
return <FaCss3 />;
case "json":
return <FaList />;
case "npmignore":
return <FaNpm />;
case "html":
return <FaHtml5 />;
case "md":
return <FaMarkdown />;
default:
return <FaFile />;
}
}
export default FileIcon;
+202
View File
@@ -0,0 +1,202 @@
import { Accordion, AccordionItem } from "@nextui-org/react";
import React, { useEffect } from "react";
import TreeView, {
ITreeViewOnNodeSelectProps,
flattenTree,
} from "react-accessible-treeview";
import { AiOutlineFolder } from "react-icons/ai";
import {
IoIosArrowBack,
IoIosArrowDown,
IoIosArrowForward,
IoIosRefresh,
} from "react-icons/io";
import { useDispatch, useSelector } from "react-redux";
import { getWorkspace, selectFile } from "../services/fileService";
import { setCode, updateWorkspace } from "../state/codeSlice";
import { RootState } from "../store";
import FileIcon from "./FileIcons";
import FolderIcon from "./FolderIcon";
import IconButton, { IconButtonProps } from "./IconButton";
interface FilesProps {
setSelectedFileName: React.Dispatch<React.SetStateAction<string>>;
setExplorerOpen: React.Dispatch<React.SetStateAction<boolean>>;
explorerOpen: boolean;
}
function RefreshButton({
onClick,
ariaLabel,
}: Omit<IconButtonProps, "icon">): React.ReactElement {
return (
<IconButton
icon={
<IoIosRefresh
size={20}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
}
onClick={onClick}
ariaLabel={ariaLabel}
/>
);
}
function CloseButton({
onClick,
ariaLabel,
}: Omit<IconButtonProps, "icon">): React.ReactElement {
return (
<IconButton
icon={
<IoIosArrowBack
size={20}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
}
onClick={onClick}
ariaLabel={ariaLabel}
/>
);
}
function Files({
setSelectedFileName,
setExplorerOpen,
explorerOpen,
}: FilesProps): JSX.Element {
const dispatch = useDispatch();
const workspaceFolder = useSelector(
(state: RootState) => state.code.workspaceFolder,
);
const selectedIds = useSelector((state: RootState) => state.code.selectedIds);
const workspaceTree = flattenTree(workspaceFolder);
useEffect(() => {
getWorkspace().then((file) => dispatch(updateWorkspace(file)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (workspaceTree.length <= 1) {
<div className="h-full bg-neutral-700 border-neutral-600 items-center border-r-1 flex flex-col">
<div>No workspace found</div>
</div>;
}
if (!explorerOpen) {
return (
<div className="h-full min-w-[48px] bg-neutral-800 border-neutral-600 items-center border-r-1 flex flex-col transition-all ease-soft-spring">
<div className="flex mt-2 p-2 justify-end">
<IoIosArrowForward
size={20}
className="cursor-pointer text-neutral-600 hover:text-neutral-100 transition"
onClick={() => setExplorerOpen(true)}
/>
</div>
</div>
);
}
const handleNodeSelect = (node: ITreeViewOnNodeSelectProps) => {
if (!node.isBranch) {
let fullPath = node.element.name;
setSelectedFileName(fullPath);
let currentNode = workspaceTree.find(
(file) => file.id === node.element.id,
);
while (currentNode !== undefined && currentNode.parent) {
currentNode = workspaceTree.find(
(file) => file.id === node.element.parent,
);
fullPath = `${currentNode?.name}/${fullPath}`;
}
selectFile(fullPath).then((code) => {
dispatch(setCode(code));
});
}
};
return (
<div className="bg-neutral-800 min-w-[228px] h-full border-r-1 border-r-neutral-600 flex flex-col transition-all ease-soft-spring">
<div className="flex p-2 items-center justify-between relative">
<Accordion className="px-0" defaultExpandedKeys={["1"]} isCompact>
<AccordionItem
classNames={{
title: "editor-accordion-title",
content: "editor-accordion-content",
}}
hideIndicator
key="1"
aria-label={workspaceFolder.name}
title={
<div className="group flex items-center justify-between">
<span className="text-neutral-400 text-sm">
{workspaceFolder.name}
</span>
</div>
}
className="editor-accordion"
startContent={
<div className="flex items-center gap-1">
<IoIosArrowDown className="text-neutral-400" />
<AiOutlineFolder className="text-neutral-400" />
</div>
}
>
<div className="w-full overflow-x-auto h-full pt-[4px]">
<TreeView
className="text-sm text-neutral-400"
data={workspaceTree}
selectedIds={selectedIds}
expandedIds={workspaceTree.map((node) => node.id)}
onNodeSelect={handleNodeSelect}
// eslint-disable-next-line react/no-unstable-nested-components
nodeRenderer={({
element,
isBranch,
isExpanded,
getNodeProps,
level,
}) => (
<div
// eslint-disable-next-line react/jsx-props-no-spreading
{...getNodeProps()}
style={{ paddingLeft: 20 * (level - 1) }}
className="cursor-pointer rounded-[5px] p-1 nowrap flex items-center gap-2 aria-selected:bg-neutral-600 aria-selected:text-white hover:text-white"
>
<div className="shrink-0 pl-[48px]">
{isBranch ? (
<FolderIcon isOpen={isExpanded} />
) : (
<FileIcon filename={element.name} />
)}
</div>
{element.name}
</div>
)}
/>
</div>
</AccordionItem>
</Accordion>
<div className="transform flex h-[24px] items-center gap-1 absolute top-2 right-2">
<RefreshButton
onClick={() =>
getWorkspace().then((file) => dispatch(updateWorkspace(file)))
}
ariaLabel="Refresh"
/>
<CloseButton
onClick={() => setExplorerOpen(false)}
ariaLabel="Close Explorer"
/>
</div>
</div>
</div>
);
}
export default Files;
+16
View File
@@ -0,0 +1,16 @@
import React from "react";
import { FaFolder, FaFolderOpen } from "react-icons/fa";
interface FolderIconProps {
isOpen: boolean;
}
function FolderIcon({ isOpen }: FolderIconProps): JSX.Element {
return isOpen ? (
<FaFolderOpen color="D9D3D0" className="icon" />
) : (
<FaFolder color="D9D3D0" className="icon" />
);
}
export default FolderIcon;
+28
View File
@@ -0,0 +1,28 @@
import { Button } from "@nextui-org/react";
import React, { MouseEventHandler, ReactElement } from "react";
export interface IconButtonProps {
icon: ReactElement;
onClick: MouseEventHandler<HTMLButtonElement>;
ariaLabel: string;
}
function IconButton({
icon,
onClick,
ariaLabel,
}: IconButtonProps): React.ReactElement {
return (
<Button
type="button"
variant="flat"
onClick={onClick}
className="cursor-pointer text-[12px] bg-transparent aspect-square px-0 min-w-[20px] h-[20px]"
aria-label={ariaLabel}
>
{icon}
</Button>
);
}
export default IconButton;
+9 -8
View File
@@ -1,12 +1,13 @@
import React, { ChangeEvent, useState, KeyboardEvent } from "react";
import { useSelector } from "react-redux";
import { Textarea } from "@nextui-org/react";
import { twMerge } from "tailwind-merge";
import React, { ChangeEvent, KeyboardEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { RootState } from "../store";
import { VscSend } from "react-icons/vsc";
import { useSelector } from "react-redux";
import { twMerge } from "tailwind-merge";
import useInputComposition from "../hooks/useInputComposition";
import { sendChatMessage } from "../services/chatService";
import { I18nKey } from "../i18n/declaration";
import { sendChatMessage } from "../services/chatService";
import { RootState } from "../store";
function Input() {
const { t } = useTranslation();
@@ -36,7 +37,6 @@ function Input() {
return;
}
e.preventDefault();
e.stopPropagation();
handleSendMessage();
}
};
@@ -44,9 +44,9 @@ function Input() {
return (
<div className="w-full relative text-base">
<Textarea
disabled={!initialized}
className="py-4 px-4"
classNames={{
inputWrapper: "bg-neutral-700",
input: "pr-16 py-2",
}}
value={inputMessage}
@@ -67,8 +67,9 @@ function Input() {
)}
onClick={handleSendMessage}
disabled={!initialized}
aria-label="Send message"
>
{t(I18nKey.CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT)}
<VscSend />
</button>
</div>
);
@@ -0,0 +1,83 @@
import React from "react";
import { Button } from "@nextui-org/react";
import { fetchMsgs, clearMsgs } from "../services/session";
import { sendChatMessageFromEvent } from "../services/chatService";
import { handleAssistantMessage } from "../services/actions";
import { ResFetchMsg } from "../types/ResponseType";
import ODModal from "./ODModal";
import toast from "../utils/toast";
interface LoadMessageModalProps {
isOpen: boolean;
onClose: () => void;
}
function LoadMessageModal({
isOpen,
onClose,
}: LoadMessageModalProps): JSX.Element {
const handleStartNewSession = () => {
clearMsgs().then().catch();
onClose();
};
const handleResumeSession = async () => {
try {
const data = await fetchMsgs();
if (!data || !data.messages || data.messages.length === 0) {
return;
}
data.messages.forEach((msg: ResFetchMsg) => {
switch (msg.role) {
case "user":
sendChatMessageFromEvent(msg.payload);
break;
case "assistant":
handleAssistantMessage(msg.payload);
break;
default:
break;
}
});
onClose();
} catch (error) {
toast.stickyError("ws", "Error fetching the session");
}
};
return (
<ODModal
size="md"
isOpen={isOpen}
onClose={onClose}
hideCloseButton
backdrop="blur"
title="Unfinished Session Detected"
primaryAction={
<Button
className="bg-primary rounded-small"
onPress={handleResumeSession}
>
Resume Session
</Button>
}
secondaryAction={
<Button
className="bg-neutral-500 rounded-small"
onPress={handleStartNewSession}
>
Start New Session
</Button>
}
>
<p>
You seem to have an unfinished task. Would you like to pick up where you
left off or start fresh?
</p>
</ODModal>
);
}
export default LoadMessageModal;
+71
View File
@@ -0,0 +1,71 @@
import React from "react";
import {
ModalProps,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from "@nextui-org/react";
interface ODModalProps extends Omit<ModalProps, "children"> {
title?: string;
subtitle?: string;
primaryAction?: React.ReactNode;
secondaryAction?: React.ReactNode;
children: React.ReactNode;
isOpen: boolean;
onClose: () => void;
size: "sm" | "md";
}
function ODModal(props: ODModalProps): React.ReactElement {
const {
children,
title,
subtitle,
primaryAction,
secondaryAction,
size,
...modalProps
} = props;
return (
<Modal
className="bg-neutral-900 rounded-large"
// eslint-disable-next-line react/jsx-props-no-spreading
{...modalProps}
>
<ModalContent
className={`${size === "sm" ? "max-w-[24rem]" : "max-w-[52rem]"} p-[40px]`}
>
<ModalHeader className="flex flex-col p-0">
{title && <h3>{title}</h3>}
{subtitle && (
<span className="text-neutral-400 text-sm font-light">
{subtitle}
</span>
)}
</ModalHeader>
<ModalBody className="px-0 py-[20px]">{children}</ModalBody>
{(primaryAction || secondaryAction) && (
<ModalFooter
className={`${size === "sm" ? "flex-col" : "flex-row"} flex justify-start p-0`}
>
{primaryAction}
{secondaryAction}
</ModalFooter>
)}
</ModalContent>
</Modal>
);
}
ODModal.defaultProps = {
title: "",
subtitle: "",
primaryAction: null,
secondaryAction: null,
};
export default ODModal;
+1 -31
View File
@@ -1,37 +1,7 @@
import React from "react";
function Planner(): JSX.Element {
return (
<div className="h-full w-full bg-bg-workspace">
<h3>
Current Focus: Set up the development environment according to the
project&apos;s instructions.
</h3>
<ul className="ml-4 mt-3">
<li className="space-x-2">
<input type="checkbox" checked readOnly />
<span>
Clone the repository and review the README for project setup
instructions.
</span>
</li>
<li className="space-x-2">
<input type="checkbox" checked readOnly />
<span>
Identify the package manager and install necessary dependencies.
</span>
</li>
<li className="space-x-2">
<input type="checkbox" />
<span>
Set up the development environment according to the project&apos;s
instructions.
</span>
</li>
{/* Add more tasks */}
</ul>
</div>
);
return <div className="h-full w-full bg-neutral-700">Coming soon...</div>;
}
export default Planner;
+90
View File
@@ -0,0 +1,90 @@
import React, { useEffect, useRef, useState } from "react";
import { twMerge } from "tailwind-merge";
export enum Orientation {
HORIZONTAL = "horizontal",
VERTICAL = "vertical",
}
type ContainerProps = {
firstChild: React.ReactNode;
firstClassName: string | undefined;
secondChild: React.ReactNode;
secondClassName: string | undefined;
className: string | undefined;
orientation: Orientation;
initialSize: number;
};
export function Container({
firstChild,
firstClassName,
secondChild,
secondClassName,
className,
orientation,
initialSize,
}: ContainerProps): JSX.Element {
const [firstSize, setFirstSize] = useState<number | undefined>(initialSize);
const [dividerPosition, setDividerPosition] = useState<undefined | number>(
undefined,
);
const firstRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (firstRef.current !== null) {
if (orientation === Orientation.HORIZONTAL) {
firstRef.current.style.width = `${firstSize}px`;
} else {
firstRef.current.style.height = `${firstSize}px`;
}
}
}, [firstSize, orientation]);
const onMouseMove = (e: MouseEvent) => {
e.preventDefault();
if (firstSize && dividerPosition) {
if (orientation === Orientation.HORIZONTAL) {
const newLeftWidth = firstSize + e.clientX - dividerPosition;
setDividerPosition(e.clientX);
setFirstSize(newLeftWidth);
} else {
const newTopHeight = firstSize + e.clientY - dividerPosition;
setDividerPosition(e.clientY);
setFirstSize(newTopHeight);
}
}
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
const onMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
setDividerPosition(
orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY,
);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
return (
<div
className={twMerge(
`flex ${orientation === Orientation.HORIZONTAL ? "" : "flex-col"}`,
className,
)}
>
<div ref={firstRef} className={firstClassName}>
{firstChild}
</div>
<div
className={`${orientation === Orientation.VERTICAL ? "cursor-ns-resize h-3" : "cursor-ew-resize w-3"} shrink-0`}
onMouseDown={onMouseDown}
/>
<div className={twMerge(secondClassName, "flex-1")}>{secondChild}</div>
</div>
);
}
+114 -142
View File
@@ -1,39 +1,26 @@
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Input,
Button,
Autocomplete,
AutocompleteItem,
Button,
Select,
SelectItem,
} from "@nextui-org/react";
import { KeyboardEvent } from "@react-types/shared/src/events";
import { useTranslation } from "react-i18next";
import i18next from "i18next";
import {
INITIAL_AGENTS,
fetchModels,
fetchAgents,
fetchModels,
INITIAL_AGENTS,
INITIAL_MODELS,
sendSettings,
getInitialModel,
saveSettings,
} from "../services/settingsService";
import {
setModel,
setAgent,
setWorkspaceDirectory,
setLanguage,
} from "../state/settingsSlice";
import store, { RootState } from "../store";
import socket from "../socket/socket";
import { RootState } from "../store";
import { I18nKey } from "../i18n/declaration";
import { AvailableLanguages } from "../i18n";
import { ArgConfigType } from "../types/ConfigType";
import ODModal from "./ODModal";
interface Props {
isOpen: boolean;
@@ -47,14 +34,16 @@ const cachedAgents = JSON.parse(
localStorage.getItem("supportedAgents") || "[]",
);
function SettingModal({ isOpen, onClose }: Props): JSX.Element {
const { t } = useTranslation();
const model = useSelector((state: RootState) => state.settings.model);
const agent = useSelector((state: RootState) => state.settings.agent);
const workspaceDirectory = useSelector(
(state: RootState) => state.settings.workspaceDirectory,
function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
const settings = useSelector((state: RootState) => state.settings);
const [model, setModel] = useState(settings[ArgConfigType.LLM_MODEL]);
const [inputModel, setInputModel] = useState(
settings[ArgConfigType.LLM_MODEL],
);
const language = useSelector((state: RootState) => state.settings.language);
const [agent, setAgent] = useState(settings[ArgConfigType.AGENT]);
const [language, setLanguage] = useState(settings[ArgConfigType.LANGUAGE]);
const { t } = useTranslation();
const [supportedModels, setSupportedModels] = useState(
cachedModels.length > 0 ? cachedModels : INITIAL_MODELS,
@@ -64,16 +53,12 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
);
useEffect(() => {
async function setInitialModel() {
const initialModel = await getInitialModel();
store.dispatch(setModel(initialModel));
}
setInitialModel();
fetchModels().then((fetchedModels) => {
setSupportedModels(fetchedModels);
localStorage.setItem("supportedModels", JSON.stringify(fetchedModels));
const sortedModels = fetchedModels.sort(); // Sorting the models alphabetically
setSupportedModels(sortedModels);
localStorage.setItem("supportedModels", JSON.stringify(sortedModels));
});
fetchAgents().then((fetchedAgents) => {
setSupportedAgents(fetchedAgents);
localStorage.setItem("supportedAgents", JSON.stringify(fetchedAgents));
@@ -81,24 +66,17 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
}, []);
const handleSaveCfg = () => {
const previousModel = localStorage.getItem("model");
const previousWorkspaceDirectory =
localStorage.getItem("workspaceDirectory");
const previousAgent = localStorage.getItem("agent");
if (
model !== previousModel ||
agent !== previousAgent ||
workspaceDirectory !== previousWorkspaceDirectory
) {
sendSettings(socket, { model, agent, workspaceDirectory, language });
}
localStorage.setItem("model", model);
localStorage.setItem("workspaceDirectory", workspaceDirectory);
localStorage.setItem("agent", agent);
localStorage.setItem("language", language);
i18next.changeLanguage(language);
saveSettings(
{
[ArgConfigType.LLM_MODEL]: model ?? inputModel,
[ArgConfigType.AGENT]: agent,
[ArgConfigType.LANGUAGE]: language,
},
Object.fromEntries(
Object.entries(settings).map(([key, value]) => [key, value]),
),
false,
);
onClose();
};
@@ -106,96 +84,90 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
item.toLowerCase().includes(input.toLowerCase());
return (
<Modal isOpen={isOpen} onClose={onClose} hideCloseButton backdrop="blur">
<ModalContent>
<>
<ModalHeader className="flex flex-col gap-1">
{t(I18nKey.CONFIGURATION$MODAL_TITLE)}
</ModalHeader>
<ModalBody>
<Input
type="text"
label={t(
I18nKey.CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_LABEL,
)}
defaultValue={workspaceDirectory}
placeholder={t(
I18nKey.CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER,
)}
onChange={(e) =>
store.dispatch(setWorkspaceDirectory(e.target.value))
}
/>
<Autocomplete
defaultItems={supportedModels.map((v: string) => ({
label: v,
value: v,
}))}
label={t(I18nKey.CONFIGURATION$MODEL_SELECT_LABEL)}
placeholder={t(I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER)}
selectedKey={model}
onSelectionChange={(key) => {
store.dispatch(setModel(key as string));
}}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
>
{(item: { label: string; value: string }) => (
<AutocompleteItem key={item.value} value={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
<Autocomplete
defaultItems={supportedAgents.map((v: string) => ({
label: v,
value: v,
}))}
label={t(I18nKey.CONFIGURATION$AGENT_SELECT_LABEL)}
placeholder={t(I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER)}
defaultSelectedKey={agent}
onSelectionChange={(key) => {
store.dispatch(setAgent(key as string));
}}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
>
{(item: { label: string; value: string }) => (
<AutocompleteItem key={item.value} value={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
<Select
selectionMode="single"
onChange={(e) => {
store.dispatch(setLanguage(e.target.value));
}}
selectedKeys={[language]}
label={t(I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL)}
>
{AvailableLanguages.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
))}
</Select>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
{t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL)}
</Button>
<Button color="primary" onPress={handleSaveCfg}>
{t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL)}
</Button>
</ModalFooter>
</>
</ModalContent>
</Modal>
<ODModal
isOpen={isOpen}
onClose={onClose}
title={t(I18nKey.CONFIGURATION$MODAL_TITLE)}
subtitle={t(I18nKey.CONFIGURATION$MODAL_SUB_TITLE)}
hideCloseButton
backdrop="blur"
size="sm"
primaryAction={
<Button className="bg-primary rounded-small" onPress={handleSaveCfg}>
{t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL)}
</Button>
}
secondaryAction={
<Button className="bg-neutral-500 rounded-small" onPress={onClose}>
{t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL)}
</Button>
}
>
<>
<Autocomplete
defaultItems={supportedModels.map((v: string) => ({
label: v,
value: v,
}))}
label={t(I18nKey.CONFIGURATION$MODEL_SELECT_LABEL)}
placeholder={t(I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER)}
selectedKey={model}
onSelectionChange={(key) => {
setModel(key as string);
}}
onInputChange={(e) => setInputModel(e)}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
defaultInputValue={inputModel}
allowsCustomValue
>
{(item: { label: string; value: string }) => (
<AutocompleteItem key={item.value} value={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
<Autocomplete
defaultItems={supportedAgents.map((v: string) => ({
label: v,
value: v,
}))}
label={t(I18nKey.CONFIGURATION$AGENT_SELECT_LABEL)}
placeholder={t(I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER)}
defaultSelectedKey={agent}
onSelectionChange={(key) => {
setAgent(key as string);
}}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
>
{(item: { label: string; value: string }) => (
<AutocompleteItem key={item.value} value={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
<Select
selectionMode="single"
onChange={(e) => setLanguage(e.target.value)}
selectedKeys={[language]}
label={t(I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL)}
>
{AvailableLanguages.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
))}
</Select>
</>
</ODModal>
);
}
function SettingModal({ isOpen, onClose }: Props): JSX.Element {
// Do not render the modal if it is not open, prevents reading empty from localStorage after initialization
if (!isOpen) return <div />;
return <InnerSettingModal isOpen={isOpen} onClose={onClose} />;
}
export default SettingModal;
+32 -18
View File
@@ -1,16 +1,18 @@
import { IDisposable, Terminal as XtermTerminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import React, { useEffect, useRef } from "react";
import { VscTerminal } from "react-icons/vsc";
import { useSelector } from "react-redux";
import { FitAddon } from "xterm-addon-fit";
import socket from "../socket/socket";
import Socket from "../services/socket";
import { RootState } from "../store";
import ActionType from "../types/ActionType";
import ObservationType from "../types/ObservationType";
class JsonWebsocketAddon {
_socket: WebSocket;
_disposables: IDisposable[];
constructor(_socket: WebSocket) {
this._socket = _socket;
constructor() {
this._disposables = [];
}
@@ -18,15 +20,15 @@ class JsonWebsocketAddon {
this._disposables.push(
terminal.onData((data) => {
const payload = JSON.stringify({ action: "terminal", data });
this._socket.send(payload);
Socket.send(payload);
}),
);
this._socket.addEventListener("message", (event) => {
Socket.addEventListener("message", (event) => {
const { action, args, observation, content } = JSON.parse(event.data);
if (action === "run") {
if (action === ActionType.RUN) {
terminal.writeln(args.command);
}
if (observation === "run") {
if (observation === ObservationType.RUN) {
content.split("\n").forEach((line: string) => {
terminal.writeln(line);
});
@@ -37,7 +39,7 @@ class JsonWebsocketAddon {
dispose() {
this._disposables.forEach((d) => d.dispose());
this._socket.removeEventListener("message", () => {});
Socket.removeEventListener("message", () => {});
}
}
@@ -48,12 +50,9 @@ class JsonWebsocketAddon {
function Terminal(): JSX.Element {
const terminalRef = useRef<HTMLDivElement>(null);
const { commands } = useSelector((state: RootState) => state.cmd);
useEffect(() => {
const bgColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-workspace")
.trim();
const terminal = new XtermTerminal({
// This value is set to the appropriate value by the
// `fitAddon.fit()` call below.
@@ -64,7 +63,7 @@ function Terminal(): JSX.Element {
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
fontSize: 14,
theme: {
background: bgColor,
background: "#262626",
},
});
terminal.write("$ ");
@@ -80,17 +79,32 @@ function Terminal(): JSX.Element {
fitAddon.fit();
}, 1);
const jsonWebsocketAddon = new JsonWebsocketAddon(socket);
const jsonWebsocketAddon = new JsonWebsocketAddon();
terminal.loadAddon(jsonWebsocketAddon);
// FIXME, temporary solution to display the terminal,
// but it will rerender the terminal every time the commands change
commands.forEach((command) => {
if (command.type === "input") {
terminal.writeln(command.content);
} else {
command.content.split("\n").forEach((line: string) => {
terminal.writeln(line);
});
terminal.write("\n$ ");
}
});
return () => {
terminal.dispose();
};
}, []);
}, [commands]);
return (
<div className="flex flex-col h-full">
<div className="px-4 py-2 text-lg border-b border-border">Terminal</div>
<div className="flex items-center gap-2 px-4 py-2 text-sm border-b border-neutral-600">
<VscTerminal />
Terminal
</div>
<div className="grow p-2 flex min-h-0">
<div ref={terminalRef} className="h-full w-full" />
</div>
+21 -19
View File
@@ -1,9 +1,9 @@
import { Tab, Tabs } from "@nextui-org/react";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { IoIosGlobe } from "react-icons/io";
import { VscCode } from "react-icons/vsc";
import Calendar from "../assets/calendar";
import Earth from "../assets/earth";
import Pencil from "../assets/pencil";
import { I18nKey } from "../i18n/declaration";
import { AllTabs, TabOption, TabType } from "../types/TabOption";
import Browser from "./Browser";
@@ -23,12 +23,12 @@ function Workspace() {
},
[TabOption.CODE]: {
name: t(I18nKey.WORKSPACE$CODE_EDITOR_TAB_LABEL),
icon: <Pencil />,
icon: <VscCode size={18} />,
component: <CodeEditor key="code" />,
},
[TabOption.BROWSER]: {
name: t(I18nKey.WORKSPACE$BROWSER_TAB_LABEL),
icon: <Earth />,
icon: <IoIosGlobe size={18} />,
component: <Browser key="browser" />,
},
}),
@@ -36,23 +36,31 @@ function Workspace() {
);
return (
<>
<div className="flex flex-col min-h-0 grow">
<div
role="tablist"
className="tabs tabs-bordered tabs-lg border-b border-border"
className="tabs tabs-bordered tabs-lg border-b border-neutral-600 flex"
>
<Tabs
variant="light"
disableCursorAnimation
classNames={{
base: "w-full",
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 gap-0 h-[36px] flex",
tab: "rounded-none border-neutral-600 data-[selected=true]:bg-neutral-800 justify-start",
tabContent: "group-data-[selected=true]:text-white",
}}
size="lg"
onSelectionChange={(v) => {
setActiveTab(v as TabType);
}}
>
{AllTabs.map((tab) => (
{AllTabs.map((tab, index) => (
<Tab
key={tab}
className={`flex-grow ${index + 1 === AllTabs.length ? "" : "border-r"}`}
title={
<div className="flex items-center space-x-2">
<div className="flex grow items-center gap-2 justify-center text-xs">
{tabData[tab].icon}
<span>{tabData[tab].name}</span>
</div>
@@ -61,16 +69,10 @@ function Workspace() {
))}
</Tabs>
</div>
{Object.keys(tabData).map((tab) => (
<div
className="h-full w-full p-4 bg-bg-workspace"
key={tab}
hidden={activeTab !== tab}
>
{tabData[tab as TabType].component}
</div>
))}
</>
<div className="grow w-full bg-neutral-800 flex min-h-0">
{tabData[activeTab as TabType].component}
</div>
</div>
);
}
export default Workspace;
-15
View File
@@ -1,15 +0,0 @@
.errors {
position: fixed;
left: 50%;
transform: translateX(-50%);
top: 1rem;
z-index: 1000;
}
.error {
background-color: #B00020;
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
margin-bottom: 0.5rem;
}
@@ -0,0 +1,35 @@
import { act, renderHook } from "@testing-library/react";
import useInputComposition from "./useInputComposition";
describe("useInputComposition", () => {
it("should return isComposing as false by default", () => {
const { result } = renderHook(() => useInputComposition());
expect(result.current.isComposing).toBe(false);
});
it("should set isComposing to true when onCompositionStart is called", () => {
const { result } = renderHook(() => useInputComposition());
act(() => {
result.current.onCompositionStart();
});
expect(result.current.isComposing).toBe(true);
});
it("should set isComposing to false when onCompositionEnd is called", () => {
const { result } = renderHook(() => useInputComposition());
act(() => {
result.current.onCompositionStart();
});
expect(result.current.isComposing).toBe(true);
act(() => {
result.current.onCompositionEnd();
});
expect(result.current.isComposing).toBe(false);
});
});
+150
View File
@@ -0,0 +1,150 @@
import { renderHook, act } from "@testing-library/react";
import { useTypingEffect } from "./useTypingEffect";
jest.useFakeTimers();
describe("useTypingEffect", () => {
// This test fails because the hook improperly handles this case.
it.skip("should handle empty strings array", () => {
const { result } = renderHook(() => useTypingEffect([]));
// Immediately check the result since there's nothing to type
expect(result.current).toBe("\u00A0"); // Non-breaking space
});
it("should type out a string correctly", () => {
const message = "Hello, world! This is a test message.";
const { result } = renderHook(() => useTypingEffect([message]));
// msg.length - 2 because the first two characters are typed immediately
// 100ms per character, 0.1 playbackRate
const msToRun = (message.length - 2) * 100 * 0.1;
// Fast-forward time by to simulate typing message
act(() => {
jest.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
});
expect(result.current).toBe(message.slice(0, -1));
act(() => {
jest.advanceTimersByTime(1); // include the last character
});
expect(result.current).toBe(message);
});
it("should type of a string correctly with a different playback rate", () => {
const message = "Hello, world! This is a test message.";
const playbackRate = 0.5;
const { result } = renderHook(() =>
useTypingEffect([message], { playbackRate }),
);
const msToRun = (message.length - 2) * 100 * playbackRate;
act(() => {
jest.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
});
expect(result.current).toBe(message.slice(0, -1));
act(() => {
jest.advanceTimersByTime(1); // include the last character
});
expect(result.current).toBe(message);
});
it("should loop through strings when multiple are provided", () => {
const messages = ["Hello", "World"];
const { result } = renderHook(() => useTypingEffect(messages));
const msToRunFirstString = messages[0].length * 100 * 0.1;
// Fast-forward to end of first string
act(() => {
jest.advanceTimersByTime(msToRunFirstString);
});
expect(result.current).toBe(messages[0]); // Hello
// Fast-forward through the delay and through the second string
act(() => {
// TODO: Improve to clarify the expected timing
jest.runAllTimers();
});
expect(result.current).toBe(messages[1]); // World
});
it("should call setTypingActive with false when typing completes without loop", () => {
const setTypingActiveMock = jest.fn();
renderHook(() =>
useTypingEffect(["Hello, world!", "This is a test message."], {
loop: false,
setTypingActive: setTypingActiveMock,
}),
);
expect(setTypingActiveMock).not.toHaveBeenCalled();
act(() => {
jest.runAllTimers();
});
expect(setTypingActiveMock).toHaveBeenCalledWith(false);
expect(setTypingActiveMock).toHaveBeenCalledTimes(1);
});
it("should call addAssistantMessageToChat with the typeThis argument when typing completes without loop", () => {
const addAssistantMessageToChatMock = jest.fn();
renderHook(() =>
useTypingEffect(["Hello, world!", "This is a test message."], {
loop: false,
// Note that only "Hello, world!" is typed out (the first string in the array)
typeThis: { content: "Hello, world!", sender: "assistant" },
addAssistantMessageToChat: addAssistantMessageToChatMock,
}),
);
expect(addAssistantMessageToChatMock).not.toHaveBeenCalled();
act(() => {
jest.runAllTimers();
});
expect(addAssistantMessageToChatMock).toHaveBeenCalledTimes(1);
expect(addAssistantMessageToChatMock).toHaveBeenCalledWith({
content: "Hello, world!",
sender: "assistant",
});
});
it("should call takeOneAndType when typing completes without loop", () => {
const takeOneAndTypeMock = jest.fn();
renderHook(() =>
useTypingEffect(["Hello, world!", "This is a test message."], {
loop: false,
takeOneAndType: takeOneAndTypeMock,
}),
);
expect(takeOneAndTypeMock).not.toHaveBeenCalled();
act(() => {
jest.runAllTimers();
});
expect(takeOneAndTypeMock).toHaveBeenCalledTimes(1);
});
// Implementation is not clear on how to handle this case
it.todo("should handle typing with loop");
});
+13 -14
View File
@@ -8,26 +8,24 @@ export const useTypingEffect = (
{
loop = false,
playbackRate = 0.1,
setTypingAcitve = () => {},
setCurrentQueueMarkerState = () => {},
currentQueueMarker = 0,
setTypingActive = () => {},
addAssistantMessageToChat = () => {},
assistantMessageObj = { content: "", sender: "assistant" },
takeOneAndType = () => {},
typeThis = { content: "", sender: "assistant" },
}: {
loop?: boolean;
playbackRate?: number;
setTypingAcitve?: (bool: boolean) => void;
setCurrentQueueMarkerState?: (marker: number) => void;
currentQueueMarker?: number;
setTypingActive?: (bool: boolean) => void;
addAssistantMessageToChat?: (msg: Message) => void;
assistantMessageObj?: Message;
takeOneAndType?: () => void;
typeThis?: Message;
} = {
loop: false,
playbackRate: 0.1,
setTypingAcitve: () => {},
currentQueueMarker: 0,
setTypingActive: () => {},
addAssistantMessageToChat: () => {},
assistantMessageObj: { content: "", sender: "assistant" },
takeOneAndType: () => {},
typeThis: { content: "", sender: "assistant" },
},
) => {
// eslint-disable-next-line prefer-const
@@ -49,9 +47,9 @@ export const useTypingEffect = (
stringIndex++;
if (stringIndex === strings.length) {
if (!loop) {
setTypingAcitve(false);
setCurrentQueueMarkerState(currentQueueMarker + 1);
addAssistantMessageToChat(assistantMessageObj);
setTypingActive(false);
addAssistantMessageToChat(typeThis);
takeOneAndType();
return;
}
stringIndex = 0;
@@ -73,6 +71,7 @@ export const useTypingEffect = (
return () => {
window.clearTimeout(timeoutId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const nonBreakingSpace = "\u00A0";
+12 -3
View File
@@ -2,10 +2,19 @@ import i18n from "i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import { ArgConfigType } from "../types/ConfigType";
export const AvailableLanguages = [
{ label: "English", value: "en" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
{ label: "한국어", value: "ko-KR" },
{ label: "Norsk", value: "no" },
{ label: "Deutsch", value: "de" },
{ label: "Italiano", value: "it" },
{ label: "Português", value: "pt" },
{ label: "Español", value: "es" },
{ label: "Türkçe", value: "tr" },
];
i18n
@@ -17,17 +26,17 @@ i18n
debug: process.env.NODE_ENV === "development",
})
.then(() => {
// assume all detected languages are available
// assume all detected languages are available
const detectLanguage = i18n.language;
// cannot trust browser language setting
const settingLanguage = localStorage.getItem("language");
const settingLanguage = localStorage.getItem(ArgConfigType.LANGUAGE);
// if setting is not initialized, but detected language is available, use detected language and update language setting
if (
!settingLanguage &&
AvailableLanguages.some((lang) => detectLanguage === lang.value)
) {
localStorage.setItem("language", detectLanguage);
localStorage.setItem(ArgConfigType.LANGUAGE, detectLanguage);
i18n.changeLanguage(detectLanguage);
return;
}
+176 -20
View File
@@ -1,74 +1,230 @@
{
"WORKSPACE$TITLE": {
"en": "OpenDevin Workspace",
"zh-CN": "OpenDevin 工作区"
"zh-CN": "OpenDevin 工作区",
"de": "OpenDevin Arbeitsbereich",
"ko-KR": "OpenDevin 워크스페이스",
"no": "OpenDevin Arbeidsområde",
"zh-TW": "OpenDevin 工作區",
"it": "Area di lavoro OpenDevin",
"pt": "Espaço de trabalho OpenDevin",
"es": "Espacio de trabajo de OpenDevin",
"tr": "OpenDevin Çalışma Alanı"
},
"WORKSPACE$TERMINAL_TAB_LABEL": {
"en": "Terminal",
"zh-CN": "终端"
"zh-CN": "终端",
"de": "Terminal",
"ko-KR": "터미널",
"no": "Terminal",
"zh-TW": "終端機",
"it": "Terminale",
"pt": "Terminal",
"es": "Terminal",
"tr": "Terminal"
},
"WORKSPACE$PLANNER_TAB_LABEL": {
"en": "Planner",
"zh-CN": "规划器"
"zh-CN": "规划器",
"de": "Planer",
"ko-KR": "플래너",
"no": "Planlegger",
"zh-TW": "計畫器",
"it": "Pianificatore",
"pt": "Planejador",
"es": "Planificador",
"tr": "Planlayıcı"
},
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
"en": "Code Editor",
"zh-CN": "代码编辑器"
"zh-CN": "代码编辑器",
"de": "Code-Editor",
"ko-KR": "코드 편집기",
"no": "Kode editor",
"zh-TW": "程式碼編輯器",
"it": "Editor di codice",
"pt": "Editor de código",
"es": "Editor de código",
"tr": "Kod editörü"
},
"WORKSPACE$BROWSER_TAB_LABEL": {
"en": "Browser",
"zh-CN": "浏览器"
"zh-CN": "浏览器",
"de": "Browser",
"ko-KR": "브라우저",
"no": "Nettleser",
"zh-TW": "瀏覽器",
"it": "Browser",
"pt": "Navegador",
"es": "Navegador",
"tr": "Tarayıcı"
},
"CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_LABEL": {
"en": "OpenDevin Workspace directory",
"zh-CN": "OpenDevin 工作区目录"
"zh-CN": "OpenDevin 工作区目录",
"de": "OpenDevin Arbeitsbereichsverzeichnis",
"ko-KR": "OpenDevin 워크스페이스 폴더",
"no": "OpenDevin arbeidsmappe",
"zh-TW": "OpenDevin 工作區目錄",
"it": "Directory dell'area di lavoro OpenDevin",
"pt": "Diretório do espaço de trabalho OpenDevin",
"es": "Directorio del espacio de trabajo de OpenDevin",
"tr": "OpenDevin çalışma alanı dizini"
},
"CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER": {
"en": "Default: ./workspace",
"zh-CN": "默认:./workspace"
"zh-CN": "默认:./workspace",
"de": "Standard: ./workspace",
"ko-KR": "기본: ./workspace",
"no": "Standard: ./workspace",
"zh-TW": "默认:./workspace",
"it": "Predefinito: ./workspace",
"pt": "Padrão: ./workspace",
"es": "Predeterminado: ./workspace",
"tr":"Standart: ./workspace"
},
"CONFIGURATION$MODAL_TITLE": {
"en": "Configuration",
"zh-CN": "配置"
"zh-CN": "配置",
"de": "Konfiguration",
"ko-KR": "설정",
"no": "Konfigurasjon",
"zh-TW": "設定",
"it": "Configurazione",
"pt": "Configuração",
"es": "Configuración",
"tr": "Konfigürasyon"
},
"CONFIGURATION$MODAL_SUB_TITLE": {
"en": "Adjust settings to your liking",
"zh-CN": "根据您的喜好调整设置",
"de": "Passen Sie die Einstellungen nach Ihren Wünschen an ",
"ko-KR": "원하는 대로 설정 조정",
"no": "Juster innstillinger etter dine ønsker ",
"zh-TW": "調整設定以符合您的喜好",
"it": "Regola le impostazioni in base alle tue preferenze",
"pt": "Ajuste as configurações de acordo com sua preferência",
"es": "Ajusta la configuración a tu gusto",
"tr": "Ayarları isteğinize göre ayarlayın"
},
"CONFIGURATION$MODEL_SELECT_LABEL": {
"en": "Model",
"zh-CN": "模型"
"zh-CN": "模型",
"de": "Modell",
"ko-KR": "모델",
"no": "Modell",
"zh-TW": "模型",
"it": "Modello",
"pt": "Modelo",
"es": "Modelo",
"tr": "Model"
},
"CONFIGURATION$MODEL_SELECT_PLACEHOLDER": {
"en": "Select a model",
"zh-CN": "选择一个模型"
"zh-CN": "选择一个模型",
"de": "Wähle ein Modell",
"ko-KR": "모델 선택",
"no": "Velg en modell",
"zh-TW": "選擇模型",
"it": "Seleziona un modello",
"pt": "Selecione um modelo",
"es": "Seleccionar un modelo",
"tr": "Model Seç"
},
"CONFIGURATION$AGENT_SELECT_LABEL": {
"en": "Agent",
"zh-CN": "代理"
"zh-CN": "智能体",
"de": "Agent",
"ko-KR": "에이전트",
"no": "Agent",
"zh-TW": "智能體",
"it": "Agente",
"pt": "Agente",
"es": "Agente",
"tr": "Ajan"
},
"CONFIGURATION$AGENT_SELECT_PLACEHOLDER": {
"en": "Select a agent",
"zh-CN": "选择一个代理"
"en": "Select an agent",
"zh-CN": "选择一个智能体",
"de": "Wähle einen Agenten",
"ko-KR": "에이전트 선택",
"no": "Velg en agent",
"zh-TW": "選擇智能體",
"it": "Seleziona un agente",
"pt": "Selecione um agente",
"es": "Seleccionar un agente",
"tr": "Ajan Seç"
},
"CONFIGURATION$LANGUAGE_SELECT_LABEL": {
"en": "Language",
"zh-CN": "语言"
"zh-CN": "语言",
"de": "Sprache",
"ko-KR": "언어",
"no": "Språk",
"zh-TW": "語言",
"it": "Lingua",
"pt": "Idioma",
"es": "Idioma",
"tr": "Dil"
},
"CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL": {
"en": "Close",
"zh-CN": "关闭"
"zh-CN": "关闭",
"de": "Schließen",
"ko-KR": "닫기",
"no": "Lukk",
"zh-TW": "關閉",
"it": "Chiudi",
"pt": "Fechar",
"es": "Cerrar",
"tr": "Kapat"
},
"CONFIGURATION$MODAL_SAVE_BUTTON_LABEL": {
"en": "Save",
"zh-CN": "保存"
"zh-CN": "保存",
"de": "Speichern",
"ko-KR": "저장",
"no": "Lagre",
"zh-TW": "儲存",
"it": "Salva",
"pt": "Salvar",
"es": "Guardar",
"tr": "Kaydet"
},
"CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE": {
"en": "Initializing agent (may take up to 10 seconds)...",
"zh-CN": "初始化代理(可能需要 10 秒以上时间)"
"zh-CN": "初始化智能体(可能需要 10 秒以上时间)",
"de": "Agent wird initialisiert (kann bis zu 10 Sekunden dauern)...",
"ko-KR": "에이전트 설치중(10초 정도 걸립니다)...",
"no": "Initialiserer agent (det kan ta opptil 10 sekunder)...",
"zh-TW": "初始化智能體(可能需要 10 秒以上時間)",
"it": "Inizializzazione dell'agente (può richiedere fino a 10 secondi)...",
"pt": "Inicializando o agente (pode levar até 10 segundos)...",
"es": "Inicializando el agente (puede tardar hasta 10 segundos)...",
"tr": "Ajan başlatılıyor (bu işlem 10 saniye kadar sürebilir)..."
},
"CHAT_INTERFACE$INPUT_PLACEHOLDER": {
"en": "Send a message (won't interrupt the Assistant)",
"zh-CN": "发送消息(不会打断助理)"
"zh-CN": "发送消息(不会打断助理)",
"de": "Sende eine Nachricht (unterbricht den Assistenten nicht)",
"ko-KR": "메시지 전송(어시스턴트를 방해하지 않음)",
"no": "Send en melding (det vil ikke avbryte assistenten)",
"zh-TW": "發送訊息(不會打擾到助理)",
"it": "Invia un messaggio (non interromperà l'Assistente)",
"pt": "Envie uma mensagem (não interromperá o Assistente)",
"es": "Enviar un mensaje (no interrumpirá al Asistente)",
"tr": "Bir mesaj gönderin (Asistan Kesilmeyecek)"
},
"CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
"en": "Send",
"zh-CN": "发送"
"zh-CN": "发送",
"de": "Senden",
"ko-KR": "전송",
"no": "Send",
"zh-TW": "發送",
"it": "Invia",
"pt": "Enviar",
"es": "Enviar",
"tr": "Gönder"
}
}
}
+26 -2
View File
@@ -4,12 +4,17 @@
--bg-input: #393939;
--bg-workspace: #1f2228;
--border: #3c3c4a;
background-color: var(--bg-dark) !important;
--text-editor-base: #9099AC;
--text-editor-active:#C4CBDA;
--bg-editor-sidebar: #24272E;
--bg-editor-active: #31343D;
--border-editor-sidebar: #3C3C4A;
background-color: var(--neutral-900) !important;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
font-family: -apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
@@ -20,3 +25,22 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.editor-accordion h2 > button{
padding: 0;
}
.editor-accordion-title {
color: var(--bg-neutral-400) !important;
}
.editor-accordion-content {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.editor-accordion-content ul {
display: flex;
flex-direction: column;
justify-content: center;
}
+59
View File
@@ -0,0 +1,59 @@
import { setScreenshotSrc, setUrl } from "../state/browserSlice";
import { appendAssistantMessage } from "../state/chatSlice";
import { setCode, updatePath } from "../state/codeSlice";
import { appendInput } from "../state/commandSlice";
import { setInitialized } from "../state/taskSlice";
import store from "../store";
import { ActionMessage } from "../types/Message";
import { SocketMessage } from "../types/ResponseType";
import { handleObservationMessage } from "./observations";
import ActionType from "../types/ActionType";
const messageActions = {
[ActionType.INIT]: () => {
store.dispatch(setInitialized(true));
},
[ActionType.BROWSE]: (message: ActionMessage) => {
const { url, screenshotSrc } = message.args;
store.dispatch(setUrl(url));
store.dispatch(setScreenshotSrc(screenshotSrc));
},
[ActionType.WRITE]: (message: ActionMessage) => {
const { path, content } = message.args;
store.dispatch(updatePath(path));
store.dispatch(setCode(content));
},
[ActionType.THINK]: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.args.thought));
},
[ActionType.FINISH]: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.message));
},
[ActionType.RUN]: (message: ActionMessage) => {
store.dispatch(appendInput(message.args.command));
},
};
export function handleActionMessage(message: ActionMessage) {
if (message.action in messageActions) {
const actionFn =
messageActions[message.action as keyof typeof messageActions];
actionFn(message);
}
}
export function handleAssistantMessage(data: string | SocketMessage) {
let socketMessage: SocketMessage;
if (typeof data === "string") {
socketMessage = JSON.parse(data) as SocketMessage;
} else {
socketMessage = data;
}
if ("action" in socketMessage) {
handleActionMessage(socketMessage);
} else {
handleObservationMessage(socketMessage);
}
}
+85
View File
@@ -0,0 +1,85 @@
import * as jose from "jose";
import { fetchToken, validateToken, getToken } from "./auth";
jest.mock("jose", () => ({
decodeJwt: jest.fn(),
}));
// SUGGESTION: Prefer using msw for mocking requests (see https://mswjs.io/)
global.fetch = jest.fn(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve({ token: "newToken" }),
}),
) as jest.Mock;
describe("Auth Service", () => {
beforeEach(() => {
jest.clearAllMocks();
Storage.prototype.getItem = jest.fn();
Storage.prototype.setItem = jest.fn();
});
describe("fetchToken", () => {
it("should fetch and return a token", async () => {
const data = await fetchToken();
expect(localStorage.getItem).toHaveBeenCalledWith("token"); // Used to set Authorization header
expect(data).toEqual({ token: "newToken" });
expect(fetch).toHaveBeenCalledWith(`/api/auth`, {
headers: expect.any(Headers),
});
});
it("throws an error if response status is not 200", async () => {
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ status: 401 }),
);
await expect(fetchToken()).rejects.toThrow("Get token failed.");
});
});
describe("validateToken", () => {
it("returns true for a valid token", () => {
(jose.decodeJwt as jest.Mock).mockReturnValue({ sid: "123" });
expect(validateToken("validToken")).toBe(true);
});
it("returns false for an invalid token", () => {
(jose.decodeJwt as jest.Mock).mockReturnValue({});
expect(validateToken("invalidToken")).toBe(false);
});
it("returns false when decodeJwt throws", () => {
(jose.decodeJwt as jest.Mock).mockImplementation(() => {
throw new Error("Invalid token");
});
expect(validateToken("badToken")).toBe(false);
});
});
describe("getToken", () => {
it("returns existing valid token from localStorage", async () => {
(jose.decodeJwt as jest.Mock).mockReturnValue({ sid: "123" });
(Storage.prototype.getItem as jest.Mock).mockReturnValue("existingToken");
const token = await getToken();
expect(token).toBe("existingToken");
});
it("fetches, validates, and stores a new token when existing token is invalid", async () => {
(jose.decodeJwt as jest.Mock)
.mockReturnValueOnce({})
.mockReturnValueOnce({ sid: "123" });
const token = await getToken();
expect(token).toBe("newToken");
expect(localStorage.setItem).toHaveBeenCalledWith("token", "newToken");
});
it("throws an error when fetched token is invalid", async () => {
(jose.decodeJwt as jest.Mock).mockReturnValue({});
await expect(getToken()).rejects.toThrow("Token validation failed.");
});
});
});
+44
View File
@@ -0,0 +1,44 @@
import * as jose from "jose";
import { ResFetchToken } from "../types/ResponseType";
const fetchToken = async (): Promise<ResFetchToken> => {
const headers = new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
});
const response = await fetch(`/api/auth`, { headers });
if (response.status !== 200) {
throw new Error("Get token failed.");
}
const data: ResFetchToken = await response.json();
return data;
};
export const validateToken = (token: string): boolean => {
try {
const claims = jose.decodeJwt(token);
return !(claims.sid === undefined || claims.sid === "");
} catch (error) {
return false;
}
};
const getToken = async (): Promise<string> => {
const token = localStorage.getItem("token") ?? "";
if (validateToken(token)) {
return token;
}
const data = await fetchToken();
if (data.token === undefined || data.token === "") {
throw new Error("Get token failed.");
}
const newToken = data.token;
if (validateToken(newToken)) {
localStorage.setItem("token", newToken);
return newToken;
}
throw new Error("Token validation failed.");
};
export { getToken, fetchToken };
+29 -20
View File
@@ -1,36 +1,45 @@
import {
Message,
appeendToNewChatSequence,
appendToNewChatSequence,
appendUserMessage,
emptyOutQueuedTyping,
setCurrentQueueMarker,
setCurrentTypingMessage,
takeOneTypeIt,
toggleTypingActive,
} from "../state/chatSlice";
import socket from "../socket/socket";
import Socket from "./socket";
import store from "../store";
import ActionType from "../types/ActionType";
import { SocketMessage } from "../types/ResponseType";
import { ActionMessage } from "../types/Message";
export function sendChatMessage(message: string): void {
store.dispatch(appendUserMessage(message));
const event = { action: "start", args: { task: message } };
const event = { action: ActionType.START, args: { task: message } };
const eventString = JSON.stringify(event);
socket.send(eventString);
Socket.send(eventString);
}
export function setTypingAcitve(bool: boolean): void {
export function sendChatMessageFromEvent(event: string | SocketMessage): void {
try {
let data: ActionMessage;
if (typeof event === "string") {
data = JSON.parse(event);
} else {
data = event as ActionMessage;
}
if (data && data.args && data.args.task) {
store.dispatch(appendUserMessage(data.args.task));
}
} catch (error) {
//
}
}
export function setTypingActive(bool: boolean): void {
store.dispatch(toggleTypingActive(bool));
}
export function resetQueuedTyping(): void {
store.dispatch(emptyOutQueuedTyping());
}
export function setCurrentTypingMsgState(msg: string): void {
store.dispatch(setCurrentTypingMessage(msg));
}
export function setCurrentQueueMarkerState(index: number): void {
store.dispatch(setCurrentQueueMarker(index));
}
export function addAssistantMessageToChat(msg: Message): void {
store.dispatch(appeendToNewChatSequence(msg));
store.dispatch(appendToNewChatSequence(msg));
}
export function takeOneAndType(): void {
store.dispatch(takeOneTypeIt());
}
+19
View File
@@ -0,0 +1,19 @@
export type WorkspaceFile = {
name: string;
children?: WorkspaceFile[];
};
export async function selectFile(file: string): Promise<string> {
const res = await fetch(`/api/select-file?file=${file}`);
const data = await res.json();
if (res.status !== 200) {
throw new Error(data.error);
}
return data.code as string;
}
export async function getWorkspace(): Promise<WorkspaceFile> {
const res = await fetch("/api/refresh-files");
const data = await res.json();
return data as WorkspaceFile;
}
+25
View File
@@ -0,0 +1,25 @@
import { appendAssistantMessage } from "../state/chatSlice";
import { setUrl, setScreenshotSrc } from "../state/browserSlice";
import store from "../store";
import { ObservationMessage } from "../types/Message";
import { appendOutput } from "../state/commandSlice";
import ObservationType from "../types/ObservationType";
export function handleObservationMessage(message: ObservationMessage) {
switch (message.observation) {
case ObservationType.RUN:
store.dispatch(appendOutput(message.content));
break;
case ObservationType.BROWSE:
if (message.extras?.screenshot) {
store.dispatch(setScreenshotSrc(message.extras.screenshot));
}
if (message.extras?.url) {
store.dispatch(setUrl(message.extras.url));
}
break;
default:
store.dispatch(appendAssistantMessage(message.message));
break;
}
}
+126
View File
@@ -0,0 +1,126 @@
import {
ResDelMsg,
ResFetchMsg,
ResFetchMsgTotal,
ResFetchMsgs,
} from "../types/ResponseType";
import { clearMsgs, fetchMsgTotal, fetchMsgs } from "./session";
// SUGGESTION: Prefer using msw for mocking requests (see https://mswjs.io/)
global.fetch = jest.fn();
Storage.prototype.getItem = jest.fn();
describe("Session Service", () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
// Used to set Authorization header
expect(localStorage.getItem).toHaveBeenCalledWith("token");
});
describe("fetchMsgTotal", () => {
it("should fetch and return message total", async () => {
const expectedResult: ResFetchMsgTotal = {
msg_total: 10,
};
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve(expectedResult),
}),
);
const data = await fetchMsgTotal();
expect(fetch).toHaveBeenCalledWith(`/api/messages/total`, {
headers: expect.any(Headers),
});
expect(data).toEqual(expectedResult);
});
it("throws an error if response status is not 200", async () => {
// NOTE: The current implementation ONLY handles 200 status;
// this means throwing even with a status of 201, 204, etc.
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ status: 401 }),
);
await expect(fetchMsgTotal()).rejects.toThrow(
"Get message total failed.",
);
});
});
describe("fetchMsgs", () => {
it("should fetch and return messages", async () => {
const expectedResult: ResFetchMsgs = {
messages: [
{
id: "1",
role: "admin",
payload: {} as ResFetchMsg["payload"],
},
],
};
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve(expectedResult),
}),
);
const data = await fetchMsgs();
expect(fetch).toHaveBeenCalledWith(`/api/messages`, {
headers: expect.any(Headers),
});
expect(data).toEqual(expectedResult);
});
it("throws an error if response status is not 200", async () => {
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ status: 401 }),
);
await expect(fetchMsgs()).rejects.toThrow("Get messages failed.");
});
});
describe("clearMsgs", () => {
it("should clear messages", async () => {
const expectedResult: ResDelMsg = {
ok: "true",
};
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve(expectedResult),
}),
);
const data = await clearMsgs();
expect(fetch).toHaveBeenCalledWith(`/api/messages`, {
method: "DELETE",
headers: expect.any(Headers),
});
expect(data).toEqual(expectedResult);
});
it("throws an error if response status is not 200", async () => {
(fetch as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ status: 401 }),
);
await expect(clearMsgs()).rejects.toThrow("Delete messages failed.");
});
});
});
+49
View File
@@ -0,0 +1,49 @@
import {
ResDelMsg,
ResFetchMsgs,
ResFetchMsgTotal,
} from "../types/ResponseType";
const fetchMsgTotal = async (): Promise<ResFetchMsgTotal> => {
const headers = new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
});
const response = await fetch(`/api/messages/total`, { headers });
if (response.status !== 200) {
throw new Error("Get message total failed.");
}
const data: ResFetchMsgTotal = await response.json();
return data;
};
const fetchMsgs = async (): Promise<ResFetchMsgs> => {
const headers = new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
});
const response = await fetch(`/api/messages`, { headers });
if (response.status !== 200) {
throw new Error("Get messages failed.");
}
const data: ResFetchMsgs = await response.json();
return data;
};
const clearMsgs = async (): Promise<ResDelMsg> => {
const headers = new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
});
const response = await fetch(`/api/messages`, {
method: "DELETE",
headers,
});
if (response.status !== 200) {
throw new Error("Delete messages failed.");
}
const data: ResDelMsg = await response.json();
return data;
};
export { fetchMsgTotal, fetchMsgs, clearMsgs };
@@ -0,0 +1,109 @@
import { mergeAndUpdateSettings } from "./settingsService";
import { ArgConfigType } from "../types/ConfigType";
// We need to mock this to avoid `SyntaxError` from using `Socket` in `settingsService` during testing
jest.mock("./socket", () => ({
send: jest.fn(),
}));
describe("mergeAndUpdateSettings", () => {
it("should return initial settings if newSettings is empty", () => {
const oldSettings = { key1: "value1" };
const isInit = false;
const result = mergeAndUpdateSettings({}, oldSettings, isInit);
expect(result.mergedSettings).toEqual(oldSettings);
expect(result.updatedSettings).toEqual({});
});
it("should add new keys to mergedSettings and updatedSettings", () => {
const oldSettings = { key1: "value1" };
const newSettings = { key2: "value2" };
const isInit = false;
const result = mergeAndUpdateSettings(newSettings, oldSettings, isInit);
expect(result.mergedSettings).toEqual({
key1: "value1",
key2: "value2",
});
expect(result.updatedSettings).toEqual({
key2: "value2", // New key
});
});
it("should overwrite non-DISPLAY_MAP keys in mergedSettings", () => {
const oldSettings = { key1: "value1" };
const newSettings = { key1: "newvalue1" };
const isInit = false;
const result = mergeAndUpdateSettings(newSettings, oldSettings, isInit);
expect(result.mergedSettings).toEqual({ key1: "newvalue1" });
expect(result.updatedSettings).toEqual({});
});
it("should keep old values in mergedSettings if they are equal", () => {
const oldSettings = {
[ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview",
[ArgConfigType.AGENT]: "MonologueAgent",
};
const newSettings = {
[ArgConfigType.AGENT]: "MonologueAgent",
};
const isInit = false;
const result = mergeAndUpdateSettings(newSettings, oldSettings, isInit);
expect(result.mergedSettings).toEqual(oldSettings);
expect(result.updatedSettings).toEqual({});
});
it("should keep old values in mergedSettings if isInit is true and old value is not empty", () => {
const oldSettings = {
[ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview",
[ArgConfigType.AGENT]: "MonologueAgent",
};
const newSettings = {
[ArgConfigType.AGENT]: "MonologueAgent",
};
const isInit = true;
const result = mergeAndUpdateSettings(newSettings, oldSettings, isInit);
expect(result.mergedSettings).toEqual(oldSettings);
expect(result.updatedSettings).toEqual({});
});
it("should update mergedSettings, updatedSettings and set needToSend to true for relevant changes", () => {
const oldSettings = {
[ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview",
[ArgConfigType.AGENT]: "MonologueAgent",
key1: "value1",
};
const newSettings = {
[ArgConfigType.AGENT]: "CodeActAgent",
[ArgConfigType.LANGUAGE]: "es",
key1: "newvalue1",
key2: "value2",
};
const isInit = false;
const result = mergeAndUpdateSettings(newSettings, oldSettings, isInit);
expect(result.mergedSettings).toEqual({
[ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview",
[ArgConfigType.AGENT]: "CodeActAgent", // Updated value
[ArgConfigType.LANGUAGE]: "es", // New key added
key1: "newvalue1", // Updated value
key2: "value2", // New key added
});
expect(result.updatedSettings).toEqual({
[ArgConfigType.AGENT]: "CodeActAgent",
[ArgConfigType.LANGUAGE]: "es",
key2: "value2",
});
expect(result.needToSend).toBe(true);
});
});
+111 -30
View File
@@ -1,14 +1,22 @@
import { appendAssistantMessage } from "../state/chatSlice";
import { setInitialized } from "../state/taskSlice";
import store from "../store";
import ActionType from "../types/ActionType";
import Socket from "./socket";
import { setAllSettings, setByKey } from "../state/settingsSlice";
import { ResConfigurations } from "../types/ResponseType";
import { ArgConfigType } from "../types/ConfigType";
import toast from "../utils/toast";
export async function getInitialModel() {
if (localStorage.getItem("model")) {
return localStorage.getItem("model");
export async function fetchConfigurations(): Promise<ResConfigurations> {
const headers = new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
});
const response = await fetch(`/api/configurations`, { headers });
if (response.status !== 200) {
throw new Error("Get configurations failed.");
}
const res = await fetch("/api/default-model");
return res.json();
return (await response.json()) as ResConfigurations;
}
export async function fetchModels() {
@@ -35,32 +43,105 @@ export const INITIAL_AGENTS = ["MonologueAgent", "CodeActAgent"];
export type Agent = (typeof INITIAL_AGENTS)[number];
// Map Redux settings to socket event arguments
const SETTINGS_MAP = new Map<string, string>([
["model", "model"],
["agent", "agent_cls"],
["workspaceDirectory", "directory"],
// all available settings in the frontend
// TODO: add the values to i18n to support multi languages
const DISPLAY_MAP = new Map<string, string>([
[ArgConfigType.LLM_MODEL, "model"],
[ArgConfigType.AGENT, "agent"],
[ArgConfigType.WORKSPACE_DIR, "directory"],
[ArgConfigType.LANGUAGE, "language"],
]);
// Send settings to the server
export function sendSettings(
socket: WebSocket,
reduxSettings: { [id: string]: string },
appendMessages: boolean = true,
): void {
const socketSettings = Object.fromEntries(
Object.entries(reduxSettings).map(([setting, value]) => [
SETTINGS_MAP.get(setting) || setting,
value,
]),
type SettingsUpdateInfo = {
mergedSettings: Record<string, string>;
updatedSettings: Record<string, string>;
needToSend: boolean;
};
// Function to merge and update settings
export const mergeAndUpdateSettings = (
newSettings: Record<string, string>,
oldSettings: Record<string, string>,
isInit: boolean,
) =>
Object.keys(newSettings).reduce(
(acc, key) => {
const newValue = String(newSettings[key]);
const oldValue = oldSettings[key];
// key doesn't exist in frontend settings will be overwritten by backend settings
if (oldValue === undefined) {
acc.mergedSettings[key] = newValue;
acc.updatedSettings[key] = newValue;
return acc;
}
if (!DISPLAY_MAP.has(key)) {
acc.mergedSettings[key] = newValue;
return acc;
}
if (oldValue === newValue || (isInit && oldValue !== "")) {
acc.mergedSettings[key] = oldValue;
return acc;
}
acc.mergedSettings[key] = newValue;
acc.updatedSettings[key] = newValue;
acc.needToSend = true;
return acc;
},
{
mergedSettings: { ...oldSettings },
updatedSettings: {},
needToSend: false,
} as SettingsUpdateInfo,
);
const event = { action: "initialize", args: socketSettings };
const eventString = JSON.stringify(event);
socket.send(eventString);
store.dispatch(setInitialized(false));
if (appendMessages) {
for (const [setting, value] of Object.entries(reduxSettings)) {
store.dispatch(appendAssistantMessage(`Set ${setting} to "${value}"`));
const dispatchSettings = (updatedSettings: Record<string, string>) => {
let i = 0;
for (const [key, value] of Object.entries(updatedSettings)) {
if (DISPLAY_MAP.has(key)) {
store.dispatch(setByKey({ key, value }));
setTimeout(() => {
toast.settingsChanged(`Set ${DISPLAY_MAP.get(key)} to "${value}"`);
}, i * 500);
i += 1;
}
}
};
const sendSettings = (
mergedSettings: Record<string, string>,
needToSend: boolean,
isInit: boolean,
) => {
const settingsCopy = { ...mergedSettings };
delete settingsCopy.ALL_SETTINGS;
if (needToSend || isInit) {
const event = { action: ActionType.INIT, args: settingsCopy };
const eventString = JSON.stringify(event);
store.dispatch(setInitialized(false));
Socket.send(eventString);
}
};
// Save and send settings to the server
export function saveSettings(
newSettings: { [key: string]: string },
oldSettings: { [key: string]: string },
isInit: boolean = false,
): void {
const { mergedSettings, updatedSettings, needToSend } =
mergeAndUpdateSettings(newSettings, oldSettings, isInit);
dispatchSettings(updatedSettings);
if (isInit) {
store.dispatch(setAllSettings(JSON.stringify(newSettings)));
}
sendSettings(mergedSettings, needToSend, isInit);
}
+124
View File
@@ -0,0 +1,124 @@
// import { toast } from "sonner";
import { handleAssistantMessage } from "./actions";
import { getToken } from "./auth";
import toast from "../utils/toast";
class Socket {
private static _socket: WebSocket | null = null;
// callbacks contain a list of callable functions
// event: function, like:
// open: [function1, function2]
// message: [function1, function2]
private static callbacks: {
[K in keyof WebSocketEventMap]: ((data: WebSocketEventMap[K]) => void)[];
} = {
open: [],
message: [],
error: [],
close: [],
};
// prevent it failed in the first run, all related listen events never be called
private static isFirstRun = true;
public static tryInitialize(): void {
getToken()
.then((token) => {
Socket._initialize(token);
})
.catch(() => {
const msg = `Connection failed. Retry...`;
toast.stickyError("ws", msg);
if (this.isFirstRun) {
setTimeout(() => {
this.tryInitialize();
}, 3000);
}
});
}
private static _initialize(token: string): void {
if (Socket.isConnected()) return;
const WS_URL = `ws://${window.location.host}/ws?token=${token}`;
Socket._socket = new WebSocket(WS_URL);
Socket._socket.onopen = (e) => {
toast.stickySuccess("ws", "Connected to server.");
Socket.callbacks.open?.forEach((callback) => {
callback(e);
});
};
Socket._socket.onmessage = (e) => {
handleAssistantMessage(e.data);
};
Socket._socket.onerror = () => {
const msg = "Connection failed. Retry...";
toast.stickyError("ws", msg);
};
Socket._socket.onclose = () => {
// Reconnect after a delay
setTimeout(() => {
Socket.tryInitialize();
}, 3000); // Reconnect after 3 seconds
};
this.isFirstRun = false;
}
static isConnected(): boolean {
return (
Socket._socket !== null && Socket._socket.readyState === WebSocket.OPEN
);
}
static send(message: string): void {
if (!Socket.isConnected()) Socket.tryInitialize();
if (Socket.isConnected()) {
Socket._socket?.send(message);
} else {
const msg = "Connection failed. Retry...";
toast.stickyError("ws", msg);
}
}
static addEventListener(
event: string,
callback: (e: MessageEvent) => void,
): void {
Socket._socket?.addEventListener(
event as keyof WebSocketEventMap,
callback as (
this: WebSocket,
ev: WebSocketEventMap[keyof WebSocketEventMap],
) => never,
);
}
static removeEventListener(
event: string,
listener: (e: Event) => void,
): void {
Socket._socket?.removeEventListener(event, listener);
}
static registerCallback<K extends keyof WebSocketEventMap>(
event: K,
callbacks: ((data: WebSocketEventMap[K]) => void)[],
): void {
if (Socket.callbacks[event] === undefined) {
return;
}
Socket.callbacks[event].push(...callbacks);
}
}
Socket.tryInitialize();
export default Socket;
-39
View File
@@ -1,39 +0,0 @@
import store from "../store";
import { ActionMessage } from "../types/Message";
import { setScreenshotSrc, setUrl } from "../state/browserSlice";
import { appendAssistantMessage } from "../state/chatSlice";
import { setCode } from "../state/codeSlice";
import { setInitialized } from "../state/taskSlice";
const messageActions = {
initialize: () => {
store.dispatch(setInitialized(true));
store.dispatch(
appendAssistantMessage(
"Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
),
);
},
browse: (message: ActionMessage) => {
const { url, screenshotSrc } = message.args;
store.dispatch(setUrl(url));
store.dispatch(setScreenshotSrc(screenshotSrc));
},
write: (message: ActionMessage) => {
store.dispatch(setCode(message.args.content));
},
think: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.args.thought));
},
finish: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.message));
},
};
export function handleActionMessage(message: ActionMessage) {
if (message.action in messageActions) {
const actionFn =
messageActions[message.action as keyof typeof messageActions];
actionFn(message);
}
}
-16
View File
@@ -1,16 +0,0 @@
import { appendAssistantMessage } from "../state/chatSlice";
import { setUrl, setScreenshotSrc } from "../state/browserSlice";
import store from "../store";
import { ObservationMessage } from "../types/Message";
export function handleObservationMessage(message: ObservationMessage) {
store.dispatch(appendAssistantMessage(message.message));
if (message.observation === "browse") {
if (message.extras?.screenshot) {
store.dispatch(setScreenshotSrc(message.extras.screenshot));
}
if (message.extras?.url) {
store.dispatch(setUrl(message.extras.url));
}
}
}
-44
View File
@@ -1,44 +0,0 @@
import store from "../store";
import { ActionMessage, ObservationMessage } from "../types/Message";
import { appendError } from "../state/errorsSlice";
import { handleActionMessage } from "./actions";
import { handleObservationMessage } from "./observations";
import { sendSettings } from "../services/settingsService";
type SocketMessage = ActionMessage | ObservationMessage;
const WS_URL = `ws://${window.location.host}/ws`;
const socket = new WebSocket(WS_URL);
socket.addEventListener("open", () => {
const settingKeys = ["model", "agent", "workspaceDirectory"];
const settings = settingKeys.reduce(
(acc, key) => {
const value = localStorage.getItem(key);
if (value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>,
);
sendSettings(socket, settings, false);
});
socket.addEventListener("message", (event) => {
const socketMessage = JSON.parse(event.data) as SocketMessage;
if ("action" in socketMessage) {
handleActionMessage(socketMessage);
} else {
handleObservationMessage(socketMessage);
}
});
socket.addEventListener("error", () => {
store.dispatch(
appendError(
`Failed connection to server. Please ensure the server is reachable at ${WS_URL}.`,
),
);
});
export default socket;
+39 -28
View File
@@ -5,20 +5,23 @@ export type Message = {
sender: "user" | "assistant";
};
const initialMessages: Message[] = [];
const queuedMessages: number[] = [];
const currentQueueMarker: number = 0;
const initialMessages: Message[] = [
{
content:
"Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
sender: "assistant",
},
];
export const chatSlice = createSlice({
name: "chat",
initialState: {
messages: initialMessages,
queuedTyping: queuedMessages,
typingActive: false,
currentTypingMessage: "",
currentQueueMarker,
userMessages: initialMessages,
assistantMessages: initialMessages,
assistantMessagesTypingQueue: [] as Message[],
newChatSequence: initialMessages,
typeThis: { content: "", sender: "assistant" } as Message,
},
reducers: {
appendUserMessage: (state, action) => {
@@ -28,30 +31,40 @@ export const chatSlice = createSlice({
},
appendAssistantMessage: (state, action) => {
state.messages.push({ content: action.payload, sender: "assistant" });
state.assistantMessages.push({
content: action.payload,
sender: "assistant",
});
// state.queuedTyping.push(action.payload);
const assistantMessageIndex = state.messages.length - 1;
state.queuedTyping.push(assistantMessageIndex);
},
setCurrentQueueMarker: (state, action) => {
state.currentQueueMarker = action.payload;
if (
state.assistantMessagesTypingQueue.length > 0 ||
state.typingActive === true
) {
state.assistantMessagesTypingQueue.push({
content: action.payload,
sender: "assistant",
});
} else if (
state.assistantMessagesTypingQueue.length === 0 &&
state.typingActive === false
) {
state.typeThis = {
content: action.payload,
sender: "assistant",
};
state.typingActive = true;
}
},
toggleTypingActive: (state, action) => {
state.typingActive = action.payload;
},
emptyOutQueuedTyping: (state) => {
state.queuedTyping = [];
},
setCurrentTypingMessage: (state, action) => {
state.currentTypingMessage = action.payload;
// state.currentQueueMarker += 1;
},
appeendToNewChatSequence: (state, action) => {
appendToNewChatSequence: (state, action) => {
state.newChatSequence.push(action.payload);
},
takeOneTypeIt: (state) => {
if (state.assistantMessagesTypingQueue.length > 0) {
state.typeThis = state.assistantMessagesTypingQueue.shift() as Message;
}
},
},
});
@@ -59,10 +72,8 @@ export const {
appendUserMessage,
appendAssistantMessage,
toggleTypingActive,
emptyOutQueuedTyping,
setCurrentTypingMessage,
setCurrentQueueMarker,
appeendToNewChatSequence,
appendToNewChatSequence,
takeOneTypeIt,
} = chatSlice.actions;
export default chatSlice.reducer;
+53 -1
View File
@@ -1,17 +1,69 @@
import { createSlice } from "@reduxjs/toolkit";
import { INode, flattenTree } from "react-accessible-treeview";
import { IFlatMetadata } from "react-accessible-treeview/dist/TreeView/utils";
import { WorkspaceFile } from "../services/fileService";
export const codeSlice = createSlice({
name: "code",
initialState: {
code: "# Welcome to OpenDevin!",
selectedIds: [] as number[],
workspaceFolder: { name: "" } as WorkspaceFile,
},
reducers: {
setCode: (state, action) => {
state.code = action.payload;
},
updatePath: (state, action) => {
const path = action.payload;
const pathParts = path.split("/");
let current = state.workspaceFolder;
for (let i = 0; i < pathParts.length - 1; i += 1) {
const folderName = pathParts[i];
let folder = current.children?.find((file) => file.name === folderName);
if (!folder) {
folder = { name: folderName, children: [] };
current.children?.push(folder);
}
current = folder;
}
const fileName = pathParts[pathParts.length - 1];
if (!current.children?.find((file) => file.name === fileName)) {
current.children?.push({ name: fileName });
}
const data = flattenTree(state.workspaceFolder);
const checkPath: (
file: INode<IFlatMetadata>,
pathIndex: number,
) => boolean = (file, pathIndex) => {
if (pathIndex < 0) {
if (file.parent === null) return true;
return false;
}
if (pathIndex >= 0 && file.name !== pathParts[pathIndex]) {
return false;
}
return checkPath(
data.find((f) => f.id === file.parent)!,
pathIndex - 1,
);
};
const selected = data
.filter((file) => checkPath(file, pathParts.length - 1))
.map((file) => file.id) as number[];
state.selectedIds = selected;
},
updateWorkspace: (state, action) => {
state.workspaceFolder = action.payload;
},
},
});
export const { setCode } = codeSlice.actions;
export const { setCode, updatePath, updateWorkspace } = codeSlice.actions;
export default codeSlice.reducer;
+27
View File
@@ -0,0 +1,27 @@
import { createSlice } from "@reduxjs/toolkit";
export type Command = {
content: string;
type: "input" | "output";
};
const initialCommands: Command[] = [];
export const commandSlice = createSlice({
name: "command",
initialState: {
commands: initialCommands,
},
reducers: {
appendInput: (state, action) => {
state.commands.push({ content: action.payload, type: "input" });
},
appendOutput: (state, action) => {
state.commands.push({ content: action.payload, type: "output" });
},
},
});
export const { appendInput, appendOutput } = commandSlice.actions;
export default commandSlice.reducer;
+4 -1
View File
@@ -11,9 +11,12 @@ export const errorsSlice = createSlice({
appendError: (state, action) => {
state.errors.push(action.payload);
},
removeError: (state, action) => {
state.errors = state.errors.filter((error) => error !== action.payload);
},
},
});
export const { appendError } = errorsSlice.actions;
export const { appendError, removeError } = errorsSlice.actions;
export default errorsSlice.reducer;
+21 -18
View File
@@ -1,31 +1,34 @@
import { createSlice } from "@reduxjs/toolkit";
import i18next from "i18next";
import { ArgConfigType } from "../types/ConfigType";
export const settingsSlice = createSlice({
name: "settings",
initialState: {
model: localStorage.getItem("model") || "",
agent: localStorage.getItem("agent") || "MonologueAgent",
workspaceDirectory:
localStorage.getItem("workspaceDirectory") || "./workspace",
language: localStorage.getItem("language") || "en",
},
ALL_SETTINGS: localStorage.getItem("ALL_SETTINGS") || "",
[ArgConfigType.LLM_MODEL]:
localStorage.getItem(ArgConfigType.LLM_MODEL) || "",
[ArgConfigType.AGENT]: localStorage.getItem(ArgConfigType.AGENT) || "",
[ArgConfigType.LANGUAGE]:
localStorage.getItem(ArgConfigType.LANGUAGE) || "en",
} as { [key: string]: string },
reducers: {
setModel: (state, action) => {
state.model = action.payload;
setByKey: (state, action) => {
const { key, value } = action.payload;
state[key] = value;
localStorage.setItem(key, value);
// language is a special case for now.
if (key === ArgConfigType.LANGUAGE) {
i18next.changeLanguage(value);
}
},
setAgent: (state, action) => {
state.agent = action.payload;
},
setWorkspaceDirectory: (state, action) => {
state.workspaceDirectory = action.payload;
},
setLanguage: (state, action) => {
state.language = action.payload;
setAllSettings: (state, action) => {
state.ALL_SETTINGS = action.payload;
localStorage.setItem("ALL_SETTINGS", action.payload);
},
},
});
export const { setModel, setAgent, setWorkspaceDirectory, setLanguage } =
settingsSlice.actions;
export const { setByKey, setAllSettings } = settingsSlice.actions;
export default settingsSlice.reducer;
+2
View File
@@ -2,6 +2,7 @@ import { configureStore } from "@reduxjs/toolkit";
import browserReducer from "./state/browserSlice";
import chatReducer from "./state/chatSlice";
import codeReducer from "./state/codeSlice";
import commandReducer from "./state/commandSlice";
import taskReducer from "./state/taskSlice";
import errorsReducer from "./state/errorsSlice";
import settingsReducer from "./state/settingsSlice";
@@ -11,6 +12,7 @@ const store = configureStore({
browser: browserReducer,
chat: chatReducer,
code: codeReducer,
cmd: commandReducer,
task: taskReducer,
errors: errorsReducer,
settings: settingsReducer,
+35
View File
@@ -0,0 +1,35 @@
enum ArgConfigType {
LLM_API_KEY = "LLM_API_KEY",
LLM_BASE_URL = "LLM_BASE_URL",
WORKSPACE_DIR = "WORKSPACE_DIR",
LLM_MODEL = "LLM_MODEL",
SANDBOX_CONTAINER_IMAGE = "SANDBOX_CONTAINER_IMAGE",
RUN_AS_DEVIN = "RUN_AS_DEVIN",
LLM_EMBEDDING_MODEL = "LLM_EMBEDDING_MODEL",
LLM_NUM_RETRIES = "LLM_NUM_RETRIES",
LLM_COOLDOWN_TIME = "LLM_COOLDOWN_TIME",
DIRECTORY_REWRITE = "DIRECTORY_REWRITE",
MAX_ITERATIONS = "MAX_ITERATIONS",
MAX_CHARS = "MAX_CHARS",
AGENT = "AGENT",
LANGUAGE = "LANGUAGE",
}
const SupportedList: string[] = [
// ArgConfigType.LLM_API_KEY,
// ArgConfigType.LLM_BASE_URL,
// ArgConfigType.WORKSPACE_DIR,
ArgConfigType.LLM_MODEL,
// ArgConfigType.SANDBOX_CONTAINER_IMAGE,
// ArgConfigType.RUN_AS_DEVIN,
// ArgConfigType.LLM_EMBEDDING_MODEL,
// ArgConfigType.LLM_NUM_RETRIES,
// ArgConfigType.LLM_COOLDOWN_TIME,
// ArgConfigType.DIRECTORY_REWRITE,
// ArgConfigType.MAX_ITERATIONS,
ArgConfigType.AGENT,
ArgConfigType.LANGUAGE,
];
export { ArgConfigType, SupportedList };
+1 -1
View File
@@ -22,6 +22,6 @@ export interface ObservationMessage {
// A friendly message that can be put in the chat log
message: string;
// optional screenshoot
// optional screenshot
screenshot?: string;
}
+39
View File
@@ -0,0 +1,39 @@
import { ActionMessage, ObservationMessage } from "./Message";
interface ResConfigurations {
[key: string]: string | boolean | number;
}
interface ResFetchToken {
token: string;
}
interface ResFetchMsgTotal {
msg_total: number;
}
interface ResFetchMsg {
id: string;
role: string;
payload: SocketMessage;
}
interface ResFetchMsgs {
messages: ResFetchMsg[];
}
interface ResDelMsg {
ok: string;
}
type SocketMessage = ActionMessage | ObservationMessage;
export {
type ResConfigurations,
type ResFetchToken,
type ResFetchMsgTotal,
type ResFetchMsg,
type ResFetchMsgs,
type ResDelMsg,
type SocketMessage,
};
+1 -1
View File
@@ -6,6 +6,6 @@ enum TabOption {
type TabType = TabOption.PLANNER | TabOption.CODE | TabOption.BROWSER;
const AllTabs = [TabOption.PLANNER, TabOption.CODE, TabOption.BROWSER];
const AllTabs = [TabOption.CODE, TabOption.BROWSER];
export { AllTabs, TabOption, type TabType };

Some files were not shown because too many files have changed in this diff Show More