mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31ba53f9fe |
@@ -1,5 +0,0 @@
|
||||
frontend/node_modules
|
||||
config.toml
|
||||
.envrc
|
||||
.env
|
||||
.git
|
||||
@@ -16,11 +16,9 @@ assignees: ''
|
||||
```bash
|
||||
```
|
||||
|
||||
**My operating system**:
|
||||
|
||||
<!-- tell us everything about your environment -->
|
||||
**My environment vars and other configuration** (be sure to redact API keys):
|
||||
```bash
|
||||
**My config.toml and environment vars** (be sure to redact API keys):
|
||||
```toml
|
||||
```
|
||||
|
||||
**My model and agent** (you can see these settings in the UI):
|
||||
@@ -39,3 +37,4 @@ assignees: ''
|
||||
**Logs, error messages, and screenshots**:
|
||||
|
||||
#### Additional Context
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
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.-->
|
||||
@@ -3,45 +3,15 @@ name: Build & Run Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
on-macos:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
|
||||
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 & Start Docker
|
||||
run: |
|
||||
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:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
- name: Set up Python
|
||||
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
|
||||
python-version: '3.11'
|
||||
- name: Run tests
|
||||
run: |
|
||||
make build
|
||||
poetry run pytest ./tests
|
||||
@@ -1,109 +0,0 @@
|
||||
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."
|
||||
@@ -1,7 +1,8 @@
|
||||
name: Publish Docker Image
|
||||
name: Build and publish multi-arch container images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
@@ -13,9 +14,6 @@ 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
|
||||
@@ -31,5 +29,31 @@ 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 ${{ matrix.image }}
|
||||
run: ./containers/build.sh ${{ matrix.image }} --push
|
||||
- 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)
|
||||
|
||||
+19
-10
@@ -6,33 +6,42 @@ jobs:
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
package_json_file: frontend/package.json
|
||||
|
||||
- name: Install Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'frontend/pnpm-lock.yaml'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm install --frozen-lockfile
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint
|
||||
pnpm run lint
|
||||
|
||||
lint-python:
|
||||
name: Lint python
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.11
|
||||
- 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
|
||||
- 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/
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
@@ -198,7 +198,6 @@ logs
|
||||
.envrc
|
||||
/workspace
|
||||
/debug
|
||||
cache
|
||||
|
||||
# configuration
|
||||
config.toml
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
|
||||
# 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
|
||||
@@ -1,70 +0,0 @@
|
||||
# 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. LLM Debugging
|
||||
|
||||
If you encounter any issues with the Language Model (LM) or you're simply curious, you can inspect the actual LLM prompts and responses. To do so, export DEBUG=1 in the environment and restart the backend. OpenDevin will then log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
|
||||
|
||||
### 7. 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
|
||||
```
|
||||
@@ -6,148 +6,57 @@ BACKEND_PORT = 3000
|
||||
BACKEND_HOST = "127.0.0.1:$(BACKEND_PORT)"
|
||||
FRONTEND_PORT = 3001
|
||||
DEFAULT_WORKSPACE_DIR = "./workspace"
|
||||
DEFAULT_MODEL = "gpt-3.5-turbo-1106"
|
||||
DEFAULT_MODEL = "gpt-4-0125-preview"
|
||||
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 "$(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.11 > /dev/null; then \
|
||||
echo "$(BLUE)$(shell python3.11 --version) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Python 3.11 is not installed. Please install Python 3.11 to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
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.11 -$(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)"
|
||||
@echo "Building project..."
|
||||
@echo "Pulling Docker image..."
|
||||
@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
|
||||
@echo "Installing Python dependencies..."
|
||||
@curl -sSL https://install.python-poetry.org | python3 -
|
||||
@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
|
||||
@echo "Activating Poetry shell..."
|
||||
@echo "Installing pre-commit hooks..."
|
||||
@poetry run pre-commit install --config $(PRECOMMIT_CONFIG_PATH)
|
||||
@echo "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
|
||||
|
||||
lint:
|
||||
@echo "$(YELLOW)Running linters...$(RESET)"
|
||||
@poetry run pre-commit run --files opendevin/**/* agenthub/**/* --show-diff-on-failure --config $(PRECOMMIT_CONFIG_PATH)
|
||||
|
||||
build-frontend:
|
||||
@echo "$(YELLOW)Building frontend...$(RESET)"
|
||||
@cd frontend && npm run build
|
||||
@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; \
|
||||
fi
|
||||
@which corepack > /dev/null || (echo "Installing corepack..." && npm install -g corepack)
|
||||
@cd frontend && sudo corepack enable && pnpm install && pnpm run make-i18n
|
||||
|
||||
# Start backend
|
||||
start-backend:
|
||||
@echo "$(YELLOW)Starting backend...$(RESET)"
|
||||
@echo "Starting backend..."
|
||||
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT)
|
||||
|
||||
# Start frontend
|
||||
start-frontend:
|
||||
@echo "$(YELLOW)Starting frontend...$(RESET)"
|
||||
@cd frontend && BACKEND_HOST=$(BACKEND_HOST) FRONTEND_PORT=$(FRONTEND_PORT) npm run start
|
||||
@echo "Starting frontend..."
|
||||
@cd frontend && BACKEND_HOST=$(BACKEND_HOST) FRONTEND_PORT=$(FRONTEND_PORT) pnpm run start
|
||||
|
||||
# Run the app
|
||||
run:
|
||||
@echo "$(YELLOW)Running the app...$(RESET)"
|
||||
@echo "Running the app..."
|
||||
@if [ "$(OS)" = "Windows_NT" ]; then \
|
||||
echo "$(RED)`make run` is not supported on Windows. Please run `make start-frontend` and `make start-backend` separately.$(RESET)"; \
|
||||
echo "`make run` is not supported on Windows. Please run `make start-frontend` and `make start-backend` separately."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p logs
|
||||
@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)"
|
||||
@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..."
|
||||
@until nc -z localhost $(BACKEND_PORT); do sleep 0.1; done
|
||||
@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)"
|
||||
@cd frontend && pnpm run start -- --port $(FRONTEND_PORT)
|
||||
|
||||
# Setup config.toml
|
||||
setup-config:
|
||||
@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:
|
||||
@echo "Setting up config.toml..."
|
||||
@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
|
||||
@@ -155,17 +64,14 @@ setup-config-prompts:
|
||||
@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 (will overwrite LLM_BASE_URL): " llm_base_url; \
|
||||
read -p "Enter the local model 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 (will overwrite LLM_BASE_URL): " llm_base_url; \
|
||||
read -p "Enter the Azure endpoint 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; \
|
||||
@@ -175,21 +81,22 @@ setup-config-prompts:
|
||||
|
||||
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
|
||||
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
|
||||
echo "WORKSPACE_BASE=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
|
||||
echo "WORKSPACE_DIR=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@mv $(CONFIG_FILE).tmp $(CONFIG_FILE)
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "$(BLUE)Usage: make [target]$(RESET)"
|
||||
@echo "Usage: make [target]"
|
||||
@echo "Targets:"
|
||||
@echo " $(GREEN)build$(RESET) - Build project, including environment setup and dependencies."
|
||||
@echo " $(GREEN)lint$(RESET) - Run linters on the project."
|
||||
@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 " 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 " Backend Log file will be stored in the 'logs' directory."
|
||||
@echo " $(GREEN)help$(RESET) - Display this help message, providing information on available targets."
|
||||
@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."
|
||||
|
||||
# Phony targets
|
||||
.PHONY: build check-dependencies check-python check-npm check-docker check-poetry pull-docker-image install-python-dependencies install-frontend-dependencies install-precommit-hooks lint start-backend start-frontend run setup-config setup-config-prompts help
|
||||
.PHONY: build build-eval start-backend start-frontend run setup-config help
|
||||
@@ -1,5 +1,3 @@
|
||||
[English](README.md) | [中文](docs/README-zh.md)
|
||||
|
||||
<a name="readme-top"></a>
|
||||
<!--
|
||||
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
||||
@@ -26,9 +24,6 @@
|
||||
<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 -->
|
||||
@@ -113,59 +108,75 @@ 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
|
||||
The easiest way to run OpenDevin is inside a Docker container.
|
||||
|
||||
To start the app, run these commands, replacing `$(pwd)/workspace` with the path to the code you want OpenDevin to work with.
|
||||
```bash
|
||||
# Your OpenAI API key, or any other LLM API key
|
||||
export LLM_API_KEY="sk-..."
|
||||
Getting started with the OpenDevin project is incredibly easy. Follow these simple steps to set up and run OpenDevin on your system:
|
||||
|
||||
# The directory you want OpenDevin to modify. MUST be an absolute path!
|
||||
export WORKSPACE_DIR=$(pwd)/workspace
|
||||
### 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
|
||||
|
||||
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:0.3.1
|
||||
```
|
||||
You'll find opendevin running at `http://localhost:3000`.
|
||||
### 2. Build and Setup The Environment
|
||||
|
||||
If you want to use the (unstable!) bleeding edge, you can use `ghcr.io/opendevin/opendevin:main` as the image.
|
||||
- **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
|
||||
```
|
||||
|
||||
See [Development.md](Development.md) for instructions on running OpenDevin without Docker.
|
||||
### 3. Configuring the Language Model
|
||||
|
||||
## 🤖 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).
|
||||
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.
|
||||
|
||||
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.
|
||||
To configure the LM of your choice, follow these steps:
|
||||
|
||||
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`
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**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).
|
||||
|
||||
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
|
||||
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>
|
||||
|
||||
## ⭐️ Research Strategy
|
||||
|
||||
@@ -200,12 +211,7 @@ For details, please check [this document](./CONTRIBUTING.md).
|
||||
|
||||
## 🤖 Join Our Community
|
||||
|
||||
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!
|
||||
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!
|
||||
|
||||
🐚 **Code less, make more with OpenDevin.**
|
||||
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
# GPT-4 Examples:
|
||||
|
||||
### Notes:
|
||||
- The final result is not completed as I only gave it 10 steps, but shows very promising use of the tools.
|
||||
- This whole run costs: $0.35 using gpt-4-turbo
|
||||
- Input Tokens: 29,734
|
||||
- Completion Tokens: 1,636
|
||||
|
||||
Interesting things:
|
||||
1. Using comments as an initial placeholder for the next move and then having that output in memory is a very cool behavior.
|
||||
2. It makes very small iterative changes due to the prompting.
|
||||
3. A lot of the logic for the game is present but not put together well
|
||||
4. There are a lot of odd quirks with the code that it has written.
|
||||
5. Some odd agent behavior with putting random lines of code in random places?
|
||||
6. The structure of the feedback loop needs some improvement so the agent can see the changes it has made better in context.
|
||||
7. Might be beneficial to add in some line numbers so the model knows what lines it is looking at.
|
||||
|
||||
|
||||
```
|
||||
==============
|
||||
STEP 0
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=91, prompt_tokens=2047, total_tokens=2138)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
To start implementing Conway's Game of Life in Python using the pygame library, I first need to set up a Python script file where I can write the code. I'll create a new Python file named `game_of_life.py`. This file will serve as the base for writing the game logic and using pygame functionalities.
|
||||
|
||||
Action:
|
||||
"""
|
||||
write game_of_life.py # This is a placeholder for the game of life implementation using pygame.
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# This is a placeholder for the game of life implementation using pygame.', start=0, end=-1, thoughts="Thoughts:\nTo start implementing Conway's Game of Life in Python using the pygame library, I first need to set up a Python script file where I can write the code. I'll create a new Python file named `game_of_life.py`. This file will serve as the base for writing the game logic and using pygame functionalities.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 0:-1.
|
||||
|
||||
|
||||
==============
|
||||
STEP 1
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=101, prompt_tokens=2351, total_tokens=2452)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
The placeholder for the game of life implementation using pygame has been set up in the file `game_of_life.py`. The next step is to start implementing the basic structure of the game. This includes importing necessary libraries, initializing the pygame module, setting up the display window, and defining the main game loop. I will begin by importing pygame and initializing it, which are the foundational steps for any pygame application.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 0 0 import pygame
|
||||
|
||||
pygame.init()
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='import pygame\n\npygame.init()', start=0, end=0, thoughts='Thoughts:\nThe placeholder for the game of life implementation using pygame has been set up in the file `game_of_life.py`. The next step is to start implementing the basic structure of the game. This includes importing necessary libraries, initializing the pygame module, setting up the display window, and defining the main game loop. I will begin by importing pygame and initializing it, which are the foundational steps for any pygame application.\n\nAction:', action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 0:0.
|
||||
|
||||
|
||||
==============
|
||||
STEP 2
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=123, prompt_tokens=2552, total_tokens=2675)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
Previously, I initialized pygame in the `game_of_life.py` file. The next step is to set up the display window for the game. This involves defining the size of the window and setting the title. This is crucial as it provides the visual interface for the game. I will also add a comment to indicate that this is the setup for the display window.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 2 2
|
||||
# Setup the display window
|
||||
screen_size = (800, 600)
|
||||
screen = pygame.display.set_mode(screen_size)
|
||||
pygame.display.set_caption("Conway's Game of Life")
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# Setup the display window\nscreen_size = (800, 600)\nscreen = pygame.display.set_mode(screen_size)\npygame.display.set_caption("Conway\'s Game of Life")', start=2, end=2, thoughts='Thoughts:\nPreviously, I initialized pygame in the `game_of_life.py` file. The next step is to set up the display window for the game. This involves defining the size of the window and setting the title. This is crucial as it provides the visual interface for the game. I will also add a comment to indicate that this is the setup for the display window.\n\nAction:', action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 2:2.
|
||||
|
||||
|
||||
==============
|
||||
STEP 3
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=144, prompt_tokens=2777, total_tokens=2921)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame and setting up the display window. The next step is to define the main game loop where the game's logic will be continuously updated and rendered. This loop will handle events, update the game state, and redraw the screen. I will start by defining the main loop structure and a basic event handling mechanism to allow for quitting the game when the user presses the close button.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 5 5 # Main game loop
|
||||
running = True
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# Main game loop\nrunning = True\nwhile running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False', start=5, end=5, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame and setting up the display window. The next step is to define the main game loop where the game's logic will be continuously updated and rendered. This loop will handle events, update the game state, and redraw the screen. I will start by defining the main loop structure and a basic event handling mechanism to allow for quitting the game when the user presses the close button.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 5:5.
|
||||
|
||||
|
||||
==============
|
||||
STEP 4
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=222, prompt_tokens=3022, total_tokens=3244)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, and defining the main game loop. The next step is to implement the logic for Conway's Game of Life. This involves creating a grid to represent the game state, initializing it with a random pattern, and defining the rules for cell survival, birth, and death. I will start by defining the grid size and initializing the grid with random states.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 7 7 # Define grid size and initialize grid
|
||||
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
|
||||
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
|
||||
import random
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='# Define grid size and initialize grid\ngrid_size = (50, 50) # Grid size as a tuple (rows, columns)\ngrid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros\nimport random\nfor y in range(grid_size[0]):\n for x in range(grid_size[1]):\n grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)', start=7, end=7, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, and defining the main game loop. The next step is to implement the logic for Conway's Game of Life. This involves creating a grid to represent the game state, initializing it with a random pattern, and defining the rules for cell survival, birth, and death. I will start by defining the grid size and initializing the grid with random states.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 7:7.
|
||||
|
||||
|
||||
==============
|
||||
STEP 5
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=234, prompt_tokens=3158, total_tokens=3392)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, and initializing the grid with random states. The next step is to implement the rules for Conway's Game of Life. This involves checking each cell's neighbors to determine its next state based on the number of alive neighbors. I will start by defining a function to count the alive neighbors for a given cell.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 9 9 def count_alive_neighbors(grid, x, y):
|
||||
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
count = 0
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
|
||||
count += grid[ny][nx]
|
||||
return count
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='def count_alive_neighbors(grid, x, y):\n directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]\n count = 0\n for dx, dy in directions:\n nx, ny = x + dx, y + dy\n if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:\n count += grid[ny][nx]\n return count', start=9, end=9, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, and initializing the grid with random states. The next step is to implement the rules for Conway's Game of Life. This involves checking each cell's neighbors to determine its next state based on the number of alive neighbors. I will start by defining a function to count the alive neighbors for a given cell.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 9:9.
|
||||
|
||||
|
||||
==============
|
||||
STEP 6
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=257, prompt_tokens=3290, total_tokens=3547)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating a function to count alive neighbors. The next step is to implement the rules for Conway's Game of Life. This involves updating the grid state based on the number of alive neighbors each cell has. I will define a function `update_grid` that applies the rules of the game to update the grid for the next generation.
|
||||
|
||||
Action:
|
||||
"""
|
||||
write game_of_life.py def update_grid(grid):
|
||||
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
alive_neighbors = count_alive_neighbors(grid, x, y)
|
||||
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
|
||||
new_grid[y][x] = 1
|
||||
elif grid[y][x] == 0 and alive_neighbors == 3:
|
||||
new_grid[y][x] = 1
|
||||
return new_grid
|
||||
11
|
||||
11
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='def update_grid(grid):\n new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]\n for y in range(grid_size[0]):\n for x in range(grid_size[1]):\n alive_neighbors = count_alive_neighbors(grid, x, y)\n if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):\n new_grid[y][x] = 1\n elif grid[y][x] == 0 and alive_neighbors == 3:\n new_grid[y][x] = 1\n return new_grid', start=11, end=11, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating a function to count alive neighbors. The next step is to implement the rules for Conway's Game of Life. This involves updating the grid state based on the number of alive neighbors each cell has. I will define a function `update_grid` that applies the rules of the game to update the grid for the next generation.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 11:11.
|
||||
|
||||
|
||||
==============
|
||||
STEP 7
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=124, prompt_tokens=3416, total_tokens=3540)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 6 6
|
||||
grid = update_grid(grid)
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='grid = update_grid(grid)', start=6, end=6, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 6:6.
|
||||
|
||||
|
||||
==============
|
||||
STEP 8
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=166, prompt_tokens=3393, total_tokens=3559)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population. I need to ensure that the `update_grid` function is called within the main game loop and that the display is updated accordingly. I will start by checking the current content of the main game loop to see where to integrate the `update_grid` function.
|
||||
|
||||
Action:
|
||||
"""
|
||||
goto 6
|
||||
"""
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileReadAction(path='game_of_life.py', start=6, end=106, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population. I need to ensure that the `update_grid` function is called within the main game loop and that the display is updated accordingly. I will start by checking the current content of the main game loop to see where to integrate the `update_grid` function.\n\nAction:", action=<ActionType.READ: 'read'>)
|
||||
|
||||
OBSERVATION:
|
||||
grid = update_grid(grid)
|
||||
running = True
|
||||
# Define grid size and initialize grid
|
||||
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
|
||||
def count_alive_neighbors(grid, x, y):
|
||||
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
def update_grid(grid):
|
||||
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
alive_neighbors = count_alive_neighbors(grid, x, y)
|
||||
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
|
||||
new_grid[y][x] = 1
|
||||
elif grid[y][x] == 0 and alive_neighbors == 3:
|
||||
new_grid[y][x] = 1
|
||||
return new_grid
|
||||
count = 0
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
|
||||
count += grid[ny][nx]
|
||||
return count
|
||||
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
|
||||
import random
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
pygame.display.set_caption("Conway's Game of Life")
|
||||
pygame.init()
|
||||
|
||||
|
||||
|
||||
==============
|
||||
STEP 9
|
||||
|
||||
PLAN:
|
||||
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
|
||||
Usage(completion_tokens=174, prompt_tokens=3728, total_tokens=3902)
|
||||
|
||||
==== RAW OUTPUT ====
|
||||
Thoughts:
|
||||
From the previous actions, it seems that the basic structure for Conway's Game of Life using pygame has been set up, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to ensure that the game loop properly updates the grid and refreshes the display to show the new state of the grid in each iteration. This involves calling `pygame.display.update()` to update the entire screen and using `pygame.time.Clock()` to control the frame rate. I will start by adding these necessary updates to the game loop.
|
||||
|
||||
Action:
|
||||
"""
|
||||
edit 95 95
|
||||
pygame.display.update()
|
||||
clock = pygame.time.Clock()
|
||||
clock.tick(10) # Control the frame rate (10 frames per second)
|
||||
"""
|
||||
|
||||
==== END RAW ====
|
||||
|
||||
|
||||
ACTION:
|
||||
FileWriteAction(path='game_of_life.py', content='pygame.display.update()\n clock = pygame.time.Clock()\n clock.tick(10) # Control the frame rate (10 frames per second)', start=95, end=95, thoughts="Thoughts:\nFrom the previous actions, it seems that the basic structure for Conway's Game of Life using pygame has been set up, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to ensure that the game loop properly updates the grid and refreshes the display to show the new state of the grid in each iteration. This involves calling `pygame.display.update()` to update the entire screen and using `pygame.time.Clock()` to control the frame rate. I will start by adding these necessary updates to the game loop.\n\nAction:", action=<ActionType.WRITE: 'write'>)
|
||||
|
||||
OBSERVATION:
|
||||
WRITE OPERATION:
|
||||
You have written to "game_of_life.py" on these lines: 95:95.
|
||||
16:43:55 - opendevin:INFO: agent_controller.py:125 - Exited before finishing the task.
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Result:
|
||||
|
||||
```
|
||||
import pygame
|
||||
|
||||
# Setup the display window
|
||||
screen_size = (800, 600)
|
||||
screen = pygame.display.set_mode(screen_size)
|
||||
# Main game loop
|
||||
grid = update_grid(grid)
|
||||
running = True
|
||||
# Define grid size and initialize grid
|
||||
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
|
||||
def count_alive_neighbors(grid, x, y):
|
||||
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
def update_grid(grid):
|
||||
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
alive_neighbors = count_alive_neighbors(grid, x, y)
|
||||
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
|
||||
new_grid[y][x] = 1
|
||||
elif grid[y][x] == 0 and alive_neighbors == 3:
|
||||
new_grid[y][x] = 1
|
||||
return new_grid
|
||||
count = 0
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
|
||||
count += grid[ny][nx]
|
||||
return count
|
||||
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
|
||||
import random
|
||||
for y in range(grid_size[0]):
|
||||
for x in range(grid_size[1]):
|
||||
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
pygame.display.set_caption("Conway's Game of Life")
|
||||
pygame.init()
|
||||
# This is a placeholder for the game of life implementation using pygame.pygame.display.update()
|
||||
clock = pygame.time.Clock()
|
||||
clock.tick(10) # Control the frame rate (10 frames per second)
|
||||
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
from opendevin.agent import Agent
|
||||
from .agent import SWEAgent
|
||||
|
||||
Agent.register('SWEAgent', SWEAgent)
|
||||
@@ -1,112 +0,0 @@
|
||||
from typing import List
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
from opendevin.action import (
|
||||
Action,
|
||||
AgentThinkAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
)
|
||||
from opendevin.observation import Observation
|
||||
|
||||
from .parser import parse_command
|
||||
|
||||
from .prompts import (
|
||||
SYSTEM_MESSAGE,
|
||||
STEP_PROMPT,
|
||||
MEMORY_FORMAT,
|
||||
NO_ACTION,
|
||||
CONTEXT_PROMPT
|
||||
)
|
||||
|
||||
|
||||
class SWEAgent(Agent):
|
||||
"""
|
||||
An attempt to recreate swe_agent with output parsing, prompting style, and Application Computer Interface (ACI).
|
||||
|
||||
SWE-agent includes ACI functions like 'goto', 'search_for', 'edit', 'scroll', 'run'
|
||||
"""
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
self.memory_window = 4
|
||||
self.max_retries = 2
|
||||
self.running_memory: List[str] = []
|
||||
self.cur_file: str = ''
|
||||
self.cur_line: int = 0
|
||||
|
||||
def _remember(self, action: Action, observation: Observation) -> None:
|
||||
"""Agent has a limited memory of the few steps implemented as a queue"""
|
||||
memory = MEMORY_FORMAT(action.to_dict(), observation.to_dict())
|
||||
self.running_memory.append(memory)
|
||||
|
||||
def _think_act(self, messages: List[dict]) -> tuple[Action, str]:
|
||||
resp = self.llm.completion(
|
||||
messages=messages,
|
||||
temperature=0.05,
|
||||
)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
print(f"\033[1m\033[91m{resp['usage']}\033[0m")
|
||||
print('\n==== RAW OUTPUT ====',
|
||||
f'\033[96m{action_resp}\033[0m',
|
||||
'==== END RAW ====\n', sep='\n')
|
||||
return parse_command(action_resp, self.cur_file, self.cur_line)
|
||||
|
||||
def _update(self, action: Action) -> None:
|
||||
if isinstance(action, (FileReadAction, FileWriteAction)):
|
||||
self.cur_file = action.path
|
||||
self.cur_line = action.start
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
SWE-Agent step:
|
||||
1. Get context - past actions, custom commands, current step
|
||||
2. Perform think-act - prompt model for action and reasoning
|
||||
3. Catch errors - ensure model takes action (5 attempts max)
|
||||
"""
|
||||
for prev_action, obs in state.updated_info:
|
||||
self._remember(prev_action, obs)
|
||||
|
||||
prompt = STEP_PROMPT(
|
||||
state.plan.main_goal,
|
||||
self.cur_file,
|
||||
self.cur_line
|
||||
)
|
||||
|
||||
msgs = [
|
||||
{'content': SYSTEM_MESSAGE, 'role': 'user'},
|
||||
{'content': prompt, 'role': 'user'}
|
||||
]
|
||||
|
||||
if len(self.running_memory) > 0:
|
||||
context = CONTEXT_PROMPT(
|
||||
self.running_memory,
|
||||
self.memory_window
|
||||
)
|
||||
msgs.insert(1, {'content': context, 'role': 'user'})
|
||||
# clrs = [''] * (len(msgs)-2) + ['\033[0;36m', '\033[0;35m']
|
||||
# print('\n\n'.join([c+m['content']+'\033[0m' for c, m in zip(clrs, msgs)]))
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
start_msg_len = len(msgs)
|
||||
while not action and len(msgs) < self.max_retries + start_msg_len:
|
||||
error = NO_ACTION(thought)
|
||||
error_msg = {'content': error, 'role': 'user'}
|
||||
msgs.append(error_msg)
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
if not action:
|
||||
action = AgentThinkAction(thought)
|
||||
|
||||
self._update(action)
|
||||
self.latest_action = action
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
return [item for item in self.running_memory if query in item]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Used to reset the agent"""
|
||||
self.running_memory = []
|
||||
super().reset()
|
||||
@@ -1,164 +0,0 @@
|
||||
from opendevin.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
BrowseURLAction,
|
||||
AgentEchoAction,
|
||||
AgentThinkAction,
|
||||
)
|
||||
|
||||
import re
|
||||
|
||||
from .prompts import CUSTOM_DOCS, COMMAND_USAGE
|
||||
|
||||
# commands: exit, read, write, browse, kill, search_file, search_dir
|
||||
|
||||
no_open_file_error = AgentEchoAction(
|
||||
'You are not currently in a file. You can use the read command to open a file and then use goto to navigate through it.')
|
||||
|
||||
|
||||
def invalid_error(cmd, docs):
|
||||
return f'''ERROR:
|
||||
Invalid command structure for
|
||||
```
|
||||
{cmd}
|
||||
```
|
||||
You may have caused this error by having multiple commands within your command block.
|
||||
If so, try again by running only one of the commands:
|
||||
|
||||
Try again using this format:
|
||||
{COMMAND_USAGE[docs]}
|
||||
'''
|
||||
|
||||
|
||||
def get_action_from_string(command_string: str, path: str, line: int, thoughts: str = '') -> Action | None:
|
||||
"""
|
||||
Parses the command string to find which command the agent wants to run
|
||||
Converts the command into a proper Action and returns
|
||||
"""
|
||||
vars = command_string.split(' ')
|
||||
cmd = vars[0]
|
||||
args = [] if len(vars) == 1 else vars[1:]
|
||||
|
||||
if 'exit' == cmd:
|
||||
return AgentFinishAction()
|
||||
|
||||
elif 'think' == cmd:
|
||||
return AgentThinkAction(' '.join(args))
|
||||
|
||||
elif 'scroll_up' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
return FileReadAction(path, line + 100, line + 200, thoughts)
|
||||
|
||||
elif 'scroll_down' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
return FileReadAction(path, line - 100, line, thoughts)
|
||||
|
||||
elif 'goto' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
rex = r'^goto\s+(\d+)$'
|
||||
valid = re.match(rex, command_string)
|
||||
if valid:
|
||||
start = int(valid.group(1))
|
||||
end = start + 100
|
||||
return FileReadAction(path, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'goto'))
|
||||
|
||||
elif 'edit' == cmd:
|
||||
if not path:
|
||||
return no_open_file_error
|
||||
rex = r'^edit\s+(\d+)\s+(-?\d+)\s+(\S.*)$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
if valid:
|
||||
start = int(valid.group(1))
|
||||
end = int(valid.group(2))
|
||||
change = valid.group(3)
|
||||
if '"' == change[-1] and '"' == change[0]:
|
||||
change = change[1:-1]
|
||||
return FileWriteAction(path, change, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'edit'))
|
||||
|
||||
elif 'read' == cmd:
|
||||
rex = r'^read\s+(\S+)(?:\s+(\d+))?(?:\s+(-?\d+))?$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
if valid:
|
||||
file = valid.group(1)
|
||||
start_str = valid.group(2)
|
||||
end_str = valid.group(3)
|
||||
|
||||
start = 0 if not start_str else int(start_str)
|
||||
end = -1 if not end_str else int(end_str)
|
||||
|
||||
return FileReadAction(file, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'read'))
|
||||
|
||||
elif 'write' == cmd:
|
||||
rex = r'^write\s+(\S+)\s+(.*?)\s*(\d+)?\s*(-?\d+)?$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
|
||||
if valid:
|
||||
file = valid.group(1)
|
||||
content = valid.group(2)
|
||||
start_str = valid.group(3)
|
||||
end_str = valid.group(4)
|
||||
|
||||
start = 0 if not start_str else int(start_str)
|
||||
end = -1 if not end_str else int(end_str)
|
||||
|
||||
if '"' == content[-1] and '"' == content[0]:
|
||||
content = content[1:-1]
|
||||
|
||||
return FileWriteAction(file, content, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'write'))
|
||||
|
||||
elif 'browse' == cmd:
|
||||
return BrowseURLAction(args[0].strip())
|
||||
|
||||
elif cmd in ['search_file', 'search_dir', 'find_file']:
|
||||
rex = r'^(search_file|search_dir|find_file)\s+(\S+)(?:\s+(\S+))?$'
|
||||
valid = re.match(rex, command_string, re.DOTALL)
|
||||
if valid:
|
||||
return CmdRunAction(command_string)
|
||||
else:
|
||||
return AgentEchoAction(f'Invalid command structure for\n ```\n{command_string}\n```.\nTry again using this format:\n{CUSTOM_DOCS}')
|
||||
else:
|
||||
# check bash command
|
||||
obs = str(CmdRunAction(f'type {cmd}'))
|
||||
if obs.split(':')[-1].strip() == 'not found':
|
||||
# echo not found error for llm
|
||||
return AgentEchoAction(content=obs)
|
||||
else:
|
||||
# run valid command
|
||||
return CmdRunAction(command_string)
|
||||
|
||||
|
||||
def parse_command(input_str: str, path: str, line: int):
|
||||
"""
|
||||
Parses a given string and separates the command (enclosed in triple backticks) from any accompanying text.
|
||||
|
||||
Args:
|
||||
input_str (str): The input string to be parsed.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the command and the accompanying text (if any).
|
||||
"""
|
||||
input_str = input_str.strip()
|
||||
if '```' in input_str:
|
||||
parts = input_str.split('```')
|
||||
command_str = parts[1].strip()
|
||||
ind = 2 if len(parts) > 2 else 1
|
||||
accompanying_text = ''.join(parts[:-ind]).strip()
|
||||
action = get_action_from_string(
|
||||
command_str, path, line, accompanying_text)
|
||||
if action:
|
||||
return action, accompanying_text
|
||||
return None, input_str # used for retry
|
||||
@@ -1,205 +0,0 @@
|
||||
from opendevin.parse_commands import parse_command_file
|
||||
|
||||
DEFAULT_COMMANDS_DICT = {
|
||||
'exit': 'Executed when task is complete',
|
||||
'read <file_name> [<start_line>] [<end_line>]': 'Shows a given file\'s contents starting from <start_line> up to <end_line>. Default: start_line = 0, end_line = -1. By default the whole file will be read.',
|
||||
'write <file> <changes> [<start_line>] [<end_line>]': 'Modifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>. Default start_line = 0 and end_line = -1. Calling this with no line args will replace the whole file.',
|
||||
'browse <url>': 'Returns the text version of any url, this can be useful to look up documentation or finding issues on github',
|
||||
'scroll_up': 'Takes no arguments. This will scroll up and show you the 100 lines above your current lines',
|
||||
'scroll_down': 'Takes no arguments. This will scroll down and show you the 100 lines below your current lines',
|
||||
'edit <start_line> <end_line> <changes>': 'This will modify lines in the currently open file. use start_line and end_line to designate which lines to change and then write the multiline changes',
|
||||
'goto <line_num>': 'This will take you directly to a line and show you the 100 lines below it.',
|
||||
'<bash_command> <args>': 'You can use any bash command you need (cd, ls, rm, grep, dir, mv, wget, git, zip, etc.) with their arguments included',
|
||||
'pip install <package>': 'You can use this to import python packages. Make sure you include the correct package name when using this command.',
|
||||
'ls': 'Use the ls command to view all the files in your current directory, this is a good starting point.',
|
||||
'NOT ALLOWED': 'You cannot use interactive commands like python or node'
|
||||
}
|
||||
|
||||
COMMAND_USAGE = {
|
||||
'exit': 'Usage:\n```\nexit\n```\nExecuted when task is complete',
|
||||
'read': 'Args:\n<file_name> [<start_line>] [<end_line>]\nUsage:\n```\nread file.py\n```\nor\n```\nread example.py <start_line> <end_line>\n```\nShows a given file\'s contents starting from <start_line> up to <end_line>. Default: start_line = 0, end_line = -1. by default the whole file will be read.',
|
||||
'write': 'Args:\n<file> <changes> [<start_line>] [<end_line>]\nUsage:\n```\nwrite "def main():\n print("This is line one")" 0 2\n```\nModifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>. Default start_line = 0 and end_line = -1. Calling this with no line args will replace the whole file.',
|
||||
'edit': 'Args:\n<start_line> <end_line> <changes>\nUsage:\n```\nedit 0 1 import pandas as pd\n```\nThis will modify the current file you are in with the changes you make between the line numbers you designate',
|
||||
'goto': 'Args:\n<line_num>\nUsage:\n```\ngoto <line_num>\n```\nThis will show you the 100 lines below and including the line you specify within your current file.',
|
||||
'scroll_up': 'Usage:\n```\nscroll_up\n```\nThis will return the 100 lines above where you are currently at',
|
||||
'scroll_down': 'Usage:\n```\nscroll_down\n```\nThis will return the 100 line below where you are currently at',
|
||||
'browse': 'Args:\n<url>\nUsage:\n```\nbrowse https://github.com/OpenDevin/OpenDevin\n```\nThis will fetch the Text elements from the given url and show them to you.',
|
||||
}
|
||||
|
||||
DEFAULT_COMMANDS = '\n'.join(
|
||||
[k + ' - ' + v for k, v in DEFAULT_COMMANDS_DICT.items()])
|
||||
|
||||
CUSTOM_DOCS = parse_command_file()
|
||||
|
||||
CUSTOM_COMMANDS = f"""Custom bash commands:
|
||||
{CUSTOM_DOCS}
|
||||
"""
|
||||
|
||||
DOCUMENTATION = f"""DOCUMENTATION:
|
||||
It is recommend that you use the commands provided for interacting with files and your directory because they have been specially built for you.
|
||||
They will make it much easier for you to look at files and make changes. Using these commands will help you be better at your task.
|
||||
You can open an file by using either the read or write operations.
|
||||
- If a file already exists you should read it before making any changes. Use the `edit` command to make changes once you have read it.
|
||||
- If you are creating a new file use the write command. Use the `edit` command to make changes once you have created the new file.
|
||||
|
||||
Commands:
|
||||
{DEFAULT_COMMANDS}
|
||||
{CUSTOM_COMMANDS}
|
||||
|
||||
The following commands require an open file to be used: edit, scroll_up, scroll_down, goto
|
||||
To modify the current file use 'edit'. To move through the current file use 'goto' or 'scroll_up'/'scroll_down'
|
||||
when using write and edit do not surround the code with any "" just write the code.
|
||||
"""
|
||||
|
||||
GENERAL_GUIDELINES = '''INSTRUCTIONS:
|
||||
Now, you're going to solve this issue on your own. You can use any bash commands or custom commands you wish to complete your task. Edit all the files you need to and run any checks or tests that you want.
|
||||
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
|
||||
When you're satisfied with all of the changes you've made, you can indicate that you are done by running the exit command.
|
||||
Note however that you cannot use any interactive session commands (e.g. python, vim, node) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
|
||||
|
||||
NOTE ABOUT THE write COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
|
||||
|
||||
IMPORTANT TIPS:
|
||||
1. Reproduce the bug: Always start by trying to replicate the bug that the issue discusses. If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment and run it to ensure you can reproduce the bug. Then, start trying to fix it. When you think you've fixed the bug, re-run the bug reproduction script to make sure that the issue has indeed been resolved.
|
||||
If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file, so that you can be sure the script ran fine all the way through.
|
||||
2. Try different commands: If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it.
|
||||
3. Navigate large files: If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, you would use the 'read' command like this: 'read <file> 583'. This is a much faster way to read through the file.
|
||||
4. Handle input files: If the bug reproduction script requires inputting/reading a specific file, such as 'buggy-input.png', and you'd like to understand how to input that file, conduct a search in the existing repository code to see whether someone else has already done that. Do this by running the command: 'search_dir "buggy-input.png"'. If that doesn't work, use the Linux 'find' command.
|
||||
5. Understand your context: Always make sure to look at the currently open file and the current working directory. The currently open file might be in a different directory than the working directory.
|
||||
6. Verify your edits: When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
|
||||
7. Thoroughly test your solution: After making any changes to fix a bug, be sure to thoroughly test your solution to ensure the bug has been resolved. Re-run the bug reproduction script and verify that the issue has been addressed.
|
||||
'''
|
||||
|
||||
RESPONSE_FORMAT = '''RESPONSE FORMAT:
|
||||
This is the format of the response you will make in order to solve the current issue.
|
||||
You will be given multiple iterations to complete this task so break it into steps and solve them one by one.
|
||||
|
||||
Your output must contain the following:
|
||||
- First, thoughts about what your next action should be and plan it out.
|
||||
- You will have a memory of your thoughts so you can use this to remember things for the next step.
|
||||
- Use your thoughts to think about what you are currently doing, what you have done on prior steps and how that relates to solving the problem.
|
||||
- Second, create a piece of code that will execute your next action based on the thoughts you have.
|
||||
- Remember that you can only have one action for each thought, do not include multiple actions.
|
||||
|
||||
Your code MUST be surrounded in triple back ticks EXACTLY like this:
|
||||
```
|
||||
<code>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Adhere to the format so that the program loop continues smoothly, it is very important to only give one command per output.
|
||||
- DO NOT give more than one command within the triple backticks. This will just throw an error and nothing will happen as a result.
|
||||
- Do not give multiple code blocks, if you do only the second one will be captured and run, this might give an error if the first one was necessary.
|
||||
- To execute multiple commands you should write them down in your thoughts section so you can remember it on the next step and execute them then.
|
||||
- The only commands you are not capable of executing are interactive commands like `python` or `node` by themselves.
|
||||
- If you think that you have completed the task that has been given to you based on your previous actions and outputs then use ``` exit ``` as the command to let the system know that you are done.
|
||||
- DO NOT make any copies of your previous memories those will be provided to you at each step, making copies just wastes time and energy. Think smarter not harder.
|
||||
- The write and edit commands requires proper indentation in the content section ex. `write hw.py def hello():\n print(\'Hello World\')` this is how you would have to format your write command.
|
||||
- The white spaces matter as the code changes will be added to the code so they must have proper syntax.
|
||||
|
||||
This is a template using the format described above
|
||||
Items in <> are suggestions for you, fill them out based on the context of the problem you are solving.
|
||||
|
||||
[ FORMAT ]
|
||||
Thoughts:
|
||||
<Provide clear and concise thoughts on the next step to take, highlighting any important details or context that should be remembered.>
|
||||
<You can use multiple lines to express your thoughts>
|
||||
|
||||
Action:
|
||||
```
|
||||
<command> <params>
|
||||
```
|
||||
[ END FORMAT ]
|
||||
|
||||
Do not provide anything extra just your thought and action.
|
||||
'''
|
||||
|
||||
SYSTEM_MESSAGE = f'''SYSTEM INFO:
|
||||
You am an autonomous coding agent, here to provide solutions for coding issues.
|
||||
You have been designed to assist you with a wide range of programming tasks, from code editing and debugging to testing and deployment.
|
||||
You have access to a variety of tools and commands that you can use to help you solve problems efficiently.
|
||||
|
||||
{GENERAL_GUIDELINES}
|
||||
|
||||
{DOCUMENTATION}
|
||||
'''.strip()
|
||||
|
||||
|
||||
def NO_ACTION(latest): return f'''
|
||||
You did not include any action to take in your most recent output:
|
||||
|
||||
===== Output ======
|
||||
{latest}
|
||||
==== End Output ===
|
||||
|
||||
Remember these are the custom commands you can use:
|
||||
{DOCUMENTATION}
|
||||
|
||||
Lets try that again, it is very important that you adhere to the output format
|
||||
This time, be sure to use the exact format below, replacing anything in <> with the appropriate value(s):
|
||||
{RESPONSE_FORMAT}
|
||||
|
||||
It is crucial you use the format provided as the output will be parsed automatically.
|
||||
'''
|
||||
|
||||
|
||||
def file_info(file: str, line: int):
|
||||
if file:
|
||||
return f'''CURRENT WORKSPACE:
|
||||
Open File: {file} on line {line}
|
||||
You can use these commands with the current file:
|
||||
Navigation: `scroll_up`, `scroll_down`, and `goto <line>`
|
||||
Modification: `edit <start_line> <end_line> <changes>`
|
||||
'''
|
||||
|
||||
|
||||
def STEP_PROMPT(task, file, line_num): return f'''
|
||||
{RESPONSE_FORMAT}
|
||||
You are currently trying to complete this task:
|
||||
{task}
|
||||
|
||||
{file_info(file, line_num)}
|
||||
|
||||
Keep all of the guidelines above in mind when you are thinking and making code.
|
||||
Please come up with a thought and action based on your current task and latest steps.
|
||||
Make sure that you do not repeat the same actions, there will not be any changes in result if you do not changes anything.
|
||||
Be very strict about the formatting that you use and make sure you follow the guidelines.
|
||||
NEVER output multiple commands. ONLY take ONE STEP at a time.
|
||||
When you have completed your task run the "exit" command.
|
||||
Begin with your thought about the next step and then come up with an action to perform your thought.
|
||||
'''.strip()
|
||||
|
||||
|
||||
def unpack_dict(data: dict, restrict: list[str] = []):
|
||||
lines = []
|
||||
for key, value in data.items():
|
||||
if key in restrict:
|
||||
continue
|
||||
elif isinstance(value, dict):
|
||||
nested_str = unpack_dict(value, restrict).replace('\n', '\n ')
|
||||
val = f'{key}:' + '\n ' + f'{nested_str}'
|
||||
lines.append(val)
|
||||
else:
|
||||
lines.append(f'{key}: {value}')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def MEMORY_FORMAT(act, obs): return f'''
|
||||
Previous Action:
|
||||
{unpack_dict(act, ["content"])}
|
||||
|
||||
Output from Action:
|
||||
{unpack_dict(obs)}
|
||||
'''.strip()
|
||||
|
||||
|
||||
def CONTEXT_PROMPT(memory, window):
|
||||
res = f'These are your past {window} actions:\n'
|
||||
window_size = window if len(memory) > window else len(memory)
|
||||
cur_mems = memory[-window_size:]
|
||||
res += '===== Previous Actions =====\n'
|
||||
for idx, mem in enumerate(cur_mems):
|
||||
res += f'\nMemory {idx}:\n{mem}\n'
|
||||
res += '======= End Actions =======\n'
|
||||
res += 'Use these memories to provide additional context to the problem you are solving.\nRemember that you have already completed these steps so you do not need to perform them again.'
|
||||
return res
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
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 SWE_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', 'SWE_agent']
|
||||
__all__ = ['monologue_agent', 'codeact_agent', 'planner_agent']
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
# 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.
|
||||
|
||||
**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.*
|
||||
A minimalistic example can be found at [research/codeact/examples/run_flask_server_with_bash.py](./examples/run_flask_server_with_bash.py):
|
||||
|
||||
```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,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .codeact_agent import CodeActAgent
|
||||
|
||||
Agent.register('CodeActAgent', CodeActAgent)
|
||||
Agent.register("CodeActAgent", CodeActAgent)
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
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.agent import Agent
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.observation import (
|
||||
AgentMessageObservation,
|
||||
CmdOutputObservation,
|
||||
AgentMessageObservation,
|
||||
)
|
||||
from opendevin.parse_commands import parse_command_file
|
||||
from opendevin.state import State
|
||||
|
||||
COMMAND_DOCS = parse_command_file()
|
||||
COMMAND_SEGMENT = (
|
||||
f"""
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
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.
|
||||
SYSTEM_MESSAGE = """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>.
|
||||
@@ -43,32 +34,25 @@ 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 the following to close the shell and end the conversation:
|
||||
<execute>exit</execute>
|
||||
When you are done, execute "exit" to close the shell and end the conversation.
|
||||
"""
|
||||
|
||||
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,
|
||||
@@ -77,69 +61,47 @@ class CodeActAgent(Agent):
|
||||
Initializes a new instance of the CodeActAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- instruction (str): The instruction for the agent to execute.
|
||||
- max_steps (int): The maximum number of steps to run the 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
|
||||
stop=["</execute>"],
|
||||
temperature=0.0,
|
||||
seed=42,
|
||||
)
|
||||
action_str: str = parse_response(response)
|
||||
state.num_of_chars += sum(len(message['content'])
|
||||
for message in self.messages) + len(action_str)
|
||||
self.messages.append({'role': 'assistant', 'content': 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)
|
||||
@@ -149,9 +111,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")
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ 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,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .agent import MonologueAgent
|
||||
|
||||
Agent.register('MonologueAgent', MonologueAgent)
|
||||
Agent.register("MonologueAgent", MonologueAgent)
|
||||
|
||||
@@ -2,8 +2,6 @@ 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,
|
||||
@@ -33,177 +31,126 @@ 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):
|
||||
"""
|
||||
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] + '...'
|
||||
)
|
||||
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: str):
|
||||
"""
|
||||
Utilizes the INITIAL_THOUGHTS list to give the agent a context for it's capabilities and how to navigate the /workspace.
|
||||
Short circuited to return when already initialized.
|
||||
|
||||
Parameters:
|
||||
- task (str): The initial goal statement provided by the user
|
||||
|
||||
Raises:
|
||||
- AgentNoInstructionError: If task is not provided
|
||||
"""
|
||||
|
||||
def _initialize(self, task):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if task is None or task == '':
|
||||
raise AgentNoInstructionError()
|
||||
if task is None or task == "":
|
||||
raise ValueError("Instruction must be provided")
|
||||
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 == 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=''
|
||||
)
|
||||
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="")
|
||||
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 = ActionType.RUN
|
||||
elif thought.startswith('WRITE'):
|
||||
parts = thought.split('WRITE ')[1].split(' > ')
|
||||
output_type = "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 = ActionType.READ
|
||||
elif thought.startswith('RECALL'):
|
||||
query = thought.split('RECALL ')[1]
|
||||
output_type = "read"
|
||||
elif thought.startswith("RECALL"):
|
||||
query = thought.split("RECALL ")[1]
|
||||
action = AgentRecallAction(query=query)
|
||||
output_type = ActionType.RECALL
|
||||
elif thought.startswith('BROWSE'):
|
||||
url = thought.split('BROWSE ')[1]
|
||||
output_type = "recall"
|
||||
elif thought.startswith("BROWSE"):
|
||||
url = thought.split("BROWSE ")[1]
|
||||
action = BrowseURLAction(url=url)
|
||||
output_type = ActionType.BROWSE
|
||||
output_type = "browse"
|
||||
else:
|
||||
action = AgentThinkAction(thought=thought)
|
||||
self._add_event(action.to_memory())
|
||||
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_memory())
|
||||
self._add_event(prev_action.to_dict())
|
||||
self._add_event(obs.to_dict())
|
||||
|
||||
state.updated_info = []
|
||||
@@ -213,27 +160,13 @@ 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]:
|
||||
"""
|
||||
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)
|
||||
|
||||
def reset(self) -> None:
|
||||
super().reset()
|
||||
self.monologue = Monologue()
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
import json
|
||||
from json_repair import repair_json
|
||||
|
||||
|
||||
def my_encoder(obj):
|
||||
"""
|
||||
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'):
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
s_repaired = repair_json(s)
|
||||
return json.loads(s_repaired, **kwargs)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from threading import Thread
|
||||
|
||||
import chromadb
|
||||
from llama_index.core import Document
|
||||
from llama_index.core.retrievers import VectorIndexRetriever
|
||||
@@ -7,106 +5,75 @@ 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('LLM_BASE_URL', required=True),
|
||||
ollama_additional_kwargs={'mirostat': 0},
|
||||
model_name="llama2",
|
||||
base_url=config.get_or_error("LLM_BASE_URL"),
|
||||
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',
|
||||
api_key=config.get('LLM_API_KEY', required=True)
|
||||
model="text-embedding-ada-002"
|
||||
)
|
||||
elif embedding_strategy == 'azureopenai':
|
||||
# Need to instruct to set these env variables in documentation
|
||||
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
||||
elif embedding_strategy == "azureopenai":
|
||||
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding # Need to instruct to set these env variables in documentation
|
||||
embed_model = AzureOpenAIEmbedding(
|
||||
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),
|
||||
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"),
|
||||
)
|
||||
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: 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']
|
||||
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"]
|
||||
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)
|
||||
thread = Thread(target=self._add_doc, args=(doc,))
|
||||
thread.start() # We add the doc concurrently so we don't have to wait ~500ms for the insert
|
||||
|
||||
def _add_doc(self, doc):
|
||||
self.index.insert(doc)
|
||||
|
||||
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
|
||||
"""
|
||||
def search(self, query, k=10):
|
||||
retriever = VectorIndexRetriever(
|
||||
index=self.index,
|
||||
similarity_top_k=k,
|
||||
)
|
||||
results = retriever.retrieve(query)
|
||||
return [r.get_text() for r in results]
|
||||
|
||||
|
||||
|
||||
@@ -1,78 +1,40 @@
|
||||
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 AgentEventTypeError()
|
||||
raise ValueError("Event must be a dictionary")
|
||||
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: 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
|
||||
"""
|
||||
|
||||
def condense(self, llm):
|
||||
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(summary_resp)
|
||||
self.thoughts = prompts.parse_summary_response(strip_markdown(summary_resp))
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise RuntimeError(f'Error condensing thoughts: {e}')
|
||||
raise RuntimeError(f"Error condensing thoughts: {e}")
|
||||
|
||||
def strip_markdown(markdown_json):
|
||||
# remove markdown code block
|
||||
return markdown_json.replace('```json\n', '').replace('```', '').strip()
|
||||
@@ -1,9 +1,6 @@
|
||||
from typing import List
|
||||
|
||||
from . import json
|
||||
from json import JSONDecodeError
|
||||
|
||||
import re
|
||||
|
||||
from opendevin.action import (
|
||||
action_from_dict,
|
||||
@@ -12,10 +9,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.
|
||||
@@ -90,55 +87,35 @@ You can also use the same action and args from the source monologue.
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
def get_summarize_monologue_prompt(thoughts):
|
||||
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.'
|
||||
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]
|
||||
|
||||
return ACTION_PROMPT % {
|
||||
'task': task,
|
||||
@@ -147,48 +124,16 @@ def get_request_action_prompt(
|
||||
'hint': hint,
|
||||
}
|
||||
|
||||
|
||||
def parse_action_response(response: str) -> Action:
|
||||
"""
|
||||
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
|
||||
json_start = response.find("{")
|
||||
json_end = response.rfind("}") + 1
|
||||
response = response[json_start:json_end]
|
||||
action_dict = json.loads(response)
|
||||
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,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .agent import PlannerAgent
|
||||
|
||||
Agent.register('PlannerAgent', PlannerAgent)
|
||||
Agent.register("PlannerAgent", PlannerAgent)
|
||||
|
||||
@@ -7,44 +7,20 @@ 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 []
|
||||
|
||||
|
||||
@@ -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]] = {
|
||||
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,
|
||||
"run": CmdRunAction,
|
||||
"kill": CmdKillAction,
|
||||
"browse": BrowseURLAction,
|
||||
"read": FileReadAction,
|
||||
"write": FileWriteAction,
|
||||
"recall": AgentRecallAction,
|
||||
"think": AgentThinkAction,
|
||||
"summarize": AgentSummarizeAction,
|
||||
"finish": AgentFinishAction,
|
||||
"add_task": AddTaskAction,
|
||||
"modify_task": ModifyTaskAction,
|
||||
}
|
||||
|
||||
HISTORY_SIZE = 10
|
||||
@@ -129,39 +129,23 @@ 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]]) -> 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
|
||||
"""
|
||||
|
||||
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]):
|
||||
plan_str = json.dumps(plan.task.to_dict(), indent=2)
|
||||
sub_history = history[-HISTORY_SIZE:]
|
||||
history_dicts = []
|
||||
latest_action: Action = NullAction()
|
||||
for action, observation in sub_history:
|
||||
if not isinstance(action, NullAction):
|
||||
history_dicts.append(action.to_memory())
|
||||
history_dicts.append(action.to_dict())
|
||||
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}."
|
||||
@@ -174,30 +158,30 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
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 == 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:
|
||||
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":
|
||||
hint = "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action."
|
||||
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 = ''
|
||||
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 = ""
|
||||
|
||||
logger.info('HINT:\n' + hint, extra={'msg_type': 'INFO'})
|
||||
print_with_color("HINT:\n" + hint, "INFO")
|
||||
return prompt % {
|
||||
'task': plan.main_goal,
|
||||
'plan': plan_str,
|
||||
@@ -206,19 +190,9 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
'plan_status': plan_status,
|
||||
}
|
||||
|
||||
|
||||
def parse_response(response: str) -> Action:
|
||||
"""
|
||||
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
|
||||
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:
|
||||
@@ -226,3 +200,4 @@ def parse_response(response: str) -> Action:
|
||||
action_dict['content'] = action_dict.pop('contents')
|
||||
action = action_from_dict(action_dict)
|
||||
return action
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# This is a template. Run `cp config.toml.template config.toml` to use it.
|
||||
|
||||
LLM_API_KEY="<YOUR OPENAI API KEY>"
|
||||
WORKSPACE_DIR="./workspace"
|
||||
@@ -1,54 +0,0 @@
|
||||
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"]
|
||||
@@ -1,2 +0,0 @@
|
||||
DOCKER_REPOSITORY=ghcr.io/opendevin/opendevin
|
||||
DOCKER_BASE_DIR="."
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,2 +0,0 @@
|
||||
DOCKER_REPOSITORY=ghcr.io/opendevin/eval-swe-bench
|
||||
DOCKER_BASE_DIR=evaluation/SWE-bench
|
||||
@@ -1,2 +0,0 @@
|
||||
DOCKER_REPOSITORY=ghcr.io/opendevin/sandbox
|
||||
DOCKER_BASE_DIR="."
|
||||
@@ -1,19 +1,13 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--select=Q000'] # Q000 is the error code for single quote enforcement
|
||||
additional_dependencies:
|
||||
- flake8-quotes
|
||||
- id: double-quote-string-fixer
|
||||
- id: requirements-txt-fixer
|
||||
|
||||
- repo: https://github.com/hhatto/autopep8
|
||||
rev: v2.1.0
|
||||
@@ -28,24 +22,18 @@ repos:
|
||||
pass_filenames: false
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.3.7
|
||||
rev: v0.3.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
entry: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
|
||||
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 ]
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-setuptools, types-pyyaml, types-toml]
|
||||
additional_dependencies: [types-requests, types-setuptools]
|
||||
entry: mypy --config-file dev_config/python/mypy.ini opendevin/ agenthub/
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
> 警告:此说明文件可能已过时。应将 README.md 视为真实的来源。如果您注意到差异,请打开一个拉取请求以更新此说明文件。
|
||||
|
||||
[English](../README.md) | [中文](README-zh.md)
|
||||
|
||||
<a name="readme-top"></a>
|
||||
<!--
|
||||
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
||||
*** that would make this better, please fork the repo and create a pull request
|
||||
*** or simply open an issue with the tag "enhancement".
|
||||
*** Don't forget to give the project a star!
|
||||
*** Thanks again! Now go create something AMAZING! :D
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
<!--
|
||||
*** I'm using markdown "reference style" links for readability.
|
||||
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
||||
*** See the bottom of this document for the declaration of the reference variables
|
||||
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
||||
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
||||
-->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/graphs/contributors"><img src="https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge" alt="Contributors"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/network/members"><img src="https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge" alt="Forks"></a>
|
||||
<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 -->
|
||||
<div align="center">
|
||||
<img src="../logo.png" alt="Logo" width="200" height="200">
|
||||
<h1 align="center">OpenDevin:少写代码,多创作</h1>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- TABLE OF CONTENTS -->
|
||||
<details>
|
||||
<summary>🗂️ Table of Contents</summary>
|
||||
<ol>
|
||||
<li><a href="#-mission">🎯 Mission</a></li>
|
||||
<li><a href="#-what-is-devin">🤔 What is Devin?</a></li>
|
||||
<li><a href="#-why-opendevin">🐚 Why OpenDevin?</a></li>
|
||||
<li><a href="#-project-status">🚧 Project Status</a></li>
|
||||
<a href="#-get-started">🚀 Get Started</a>
|
||||
<ul>
|
||||
<li><a href="#1-requirements">1. Requirements</a></li>
|
||||
<li><a href="#2-build-and-setup">2. Build and Setup</a></li>
|
||||
<li><a href="#3-run-the-application">3. Run the Application</a></li>
|
||||
<li><a href="#4-individual-server-startup">4. Individual Server Startup</a></li>
|
||||
<li><a href="#5-help">5. Help</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#%EF%B8%8F-research-strategy">⭐️ Research Strategy</a></li>
|
||||
<li><a href="#-how-to-contribute">🤝 How to Contribute</a></li>
|
||||
<li><a href="#-join-our-community">🤖 Join Our Community</a></li>
|
||||
<li><a href="#%EF%B8%8F-built-with">🛠️ Built With</a></li>
|
||||
<li><a href="#-license">📜 License</a></li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
## 🎯 使命
|
||||
|
||||
[Project Demo Video](https://github.com/OpenDevin/OpenDevin/assets/38853559/71a472cc-df34-430c-8b1d-4d7286c807c9)
|
||||
|
||||
|
||||
欢迎来到 OpenDevin,一个开源项目,旨在复制 Devin,一款自主的 AI 软件工程师,能够执行复杂的工程任务,并与用户积极合作,共同进行软件开发项目。该项目立志通过开源社区的力量复制、增强和创新 Devin。
|
||||
|
||||
<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>
|
||||
|
||||
## 🤔 Devin 是什么?
|
||||
Devin 代表着一种尖端的自主代理程序,旨在应对软件工程的复杂性。它利用诸如 shell、代码编辑器和 Web 浏览器等工具的组合,展示了在软件开发中利用 LLMs(大型语言模型)的未开发潜力。我们的目标是探索和拓展 Devin 的能力,找出其优势和改进空间,以指导开源代码模型的进展。
|
||||
|
||||
<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>
|
||||
|
||||
## 🐚 为什么选择 OpenDevin?
|
||||
OpenDevin 项目源于对复制、增强和超越原始 Devin 模型的愿望。通过与开源社区的互动,我们旨在解决 Code LLMs 在实际场景中面临的挑战,创作出对社区有重大贡献并为未来进步铺平道路的作品。
|
||||
|
||||
<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>
|
||||
|
||||
## 🚧 项目状态
|
||||
|
||||
OpenDevin 目前仍在进行中,但您已经可以运行 alpha 版本来查看端到端系统的运行情况。项目团队正在积极努力实现以下关键里程碑:
|
||||
|
||||
- **用户界面(UI)**:开发用户友好的界面,包括聊天界面、演示命令的 shell 和 Web 浏览器。
|
||||
- **架构**:构建一个稳定的代理框架,具有强大的后端,可以读取、写入和运行简单的命令。
|
||||
- **代理能力**:增强代理的能力,以生成 bash 脚本、运行测试和执行其他软件工程任务。
|
||||
- **评估**:建立一个与 Devin 评估标准一致的最小评估流水线。
|
||||
|
||||
在完成 MVP 后,团队将专注于各个领域的研究,包括基础模型、专家能力、评估和代理研究。
|
||||
|
||||
<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>
|
||||
|
||||
## ⚠️ 注意事项和警告
|
||||
* OpenDevin 仍然是一个 alpha 项目。它变化很快且不稳定。我们正在努力在未来几周发布稳定版本。
|
||||
* OpenDevin 会向您配置的 LLM 发出许多提示。大多数 LLM 都需要花费金钱,请务必设置花费限制并监控使用情况。
|
||||
* OpenDevin 在 Docker 沙箱中运行 `bash` 命令,因此不应影响您的计算机。但您的工作区目录将附加到该沙箱,并且目录中的文件可能会被修改或删除。
|
||||
* 我们默认的代理目前是 MonologueAgent,具有有限的功能,但相当稳定。我们正在开发其他代理实现,包括 [SWE 代理](https://swe-agent.com/)。您可以[在这里阅读我们当前的代理集合](./docs/documentation/Agents.md)。
|
||||
|
||||
## 🚀 开始
|
||||
|
||||
开始使用 OpenDevin 项目非常简单。按照以下简单步骤在您的系统上设置和运行 OpenDevin:
|
||||
|
||||
运行 OpenDevin 最简单的方法是在 Docker 容器中。
|
||||
您可以运行:
|
||||
```bash
|
||||
# 您的 OpenAI API 密钥,或任何其他 LLM API 密钥
|
||||
export LLM_API_KEY="sk-..."
|
||||
|
||||
# 您想要 OpenDevin 修改的目录。必须是绝对路径!
|
||||
export WORKSPACE_DIR=$(pwd)/workspace
|
||||
|
||||
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
|
||||
```
|
||||
将 `$(pwd)/workspace` 替换为您希望 OpenDevin 使用的代码路径。
|
||||
|
||||
您可以在 `http://localhost:3000` 找到正在运行的 OpenDevin。
|
||||
|
||||
请参阅[Development.md](Development.md)以获取在没有 Docker 的情况下运行 OpenDevin 的说明。
|
||||
|
||||
## 🤖 LLM 后端
|
||||
OpenDevin 可以与任何 LLM 后端配合使用。
|
||||
要获取提供的 LM 提供商和模型的完整列表,请参阅
|
||||
[litellm 文档](https://docs.litellm.ai/docs/providers)。
|
||||
|
||||
`LLM_MODEL` 环境变量控制在编程交互中使用哪个模型,
|
||||
但在 OpenDevin UI 中选择模型将覆盖此设置。
|
||||
|
||||
对于某些 LLM,可能需要以下环境变量:
|
||||
* `LLM_API_KEY`
|
||||
* `LLM_BASE_URL`
|
||||
* `LLM_EMBEDDING_MODEL`
|
||||
* `LLM_DEPLOYMENT_NAME`
|
||||
* `LLM_API_VERSION`
|
||||
|
||||
**关于替代模型的说明:**
|
||||
某些替代模型可能比其他模型更具挑战性。
|
||||
不要害怕,勇敢的冒险家!我们将很快公布 LLM 特定的文档,指导您完成您的探险。
|
||||
如果您已经掌握了除 OpenAI 的 GPT 之外的模型使用技巧,
|
||||
我们鼓励您[与我们分享您的设置说明](https://github.com/OpenDevin/OpenDevin/issues/417)。
|
||||
|
||||
还有[使用 ollama 运行本地模型的文档](./docs/documentation/LOCAL_LLM_GUIDE.md)。
|
||||
|
||||
## ⭐️ 研究策略
|
||||
|
||||
利用 LLMs 实现生产级应用程序的完全复制是一个复杂的任务。我们的策略包括:
|
||||
|
||||
1. **核心技术研究:** 专注于基础研究,以了解和改进代码生成和处理的技术方面。
|
||||
2. **专业能力:** 通过数据整理、训练方法等手段增强核心组件的效能。
|
||||
3. **任务规划:** 开发能力,用于错误检测、代码库管理和优化。
|
||||
4. **评估:** 建立全面的评估指标,以更好地了解和改进我们的模型。
|
||||
|
||||
<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>
|
||||
|
||||
## 🤝 如何贡献
|
||||
|
||||
OpenDevin 是一个社区驱动的项目,我们欢迎所有人的贡献。无论您是开发人员、研究人员,还是对利用人工智能推动软件工程领域发展充满热情的人,都有许多参与方式:
|
||||
|
||||
- **代码贡献:** 帮助我们开发核心功能、前端界面或沙盒解决方案。
|
||||
- **研究和评估:** 为我们对软件工程中的 LLMs 的理解做出贡献,参与评估模型,或提出改进意见。
|
||||
- **反馈和测试:** 使用 OpenDevin 工具集,报告错误,提出功能建议,或就可用性提供反馈。
|
||||
|
||||
详情请查看[此文档](./CONTRIBUTING.md)。
|
||||
|
||||
<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>
|
||||
|
||||
## 🤖 加入我们的社区
|
||||
|
||||
现在我们既有 Slack 工作空间用于协作构建 OpenDevin,也有 Discord 服务器用于讨论与项目、LLM、Agent 等相关的任何事情。
|
||||
|
||||
* [Slack 工作空间](https://join.slack.com/t/opendevin/shared_invite/zt-2etftj1dd-X1fDL2PYIVpsmJZkqEYANw)
|
||||
* [Discord 服务器](https://discord.gg/mBuDGRzzES)
|
||||
|
||||
如果你愿意贡献,欢迎加入我们的社区(请注意,现在无需填写[表格](https://forms.gle/758d5p6Ve8r2nxxq6))。让我们一起简化软件工程!
|
||||
|
||||
🐚 **少写代码,用OpenDevin创造更多。**
|
||||
|
||||
[](https://star-history.com/#OpenDevin/OpenDevin&Date)
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
OpenDevin 使用了一系列强大的框架和库的组合,为其开发提供了坚实的基础。以下是项目中使用的关键技术:
|
||||
|
||||
       
|
||||
|
||||
请注意,这些技术的选择正在进行中,随着项目的发展,可能会添加其他技术或移除现有技术。我们致力于采用最合适和最有效的工具,以增强 OpenDevin 的功能。
|
||||
|
||||
<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>
|
||||
|
||||
## 📜 许可证
|
||||
|
||||
根据 MIT 许可证分发。有关更多信息,请参阅 [`LICENSE`](./LICENSE)。
|
||||
|
||||
<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>
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge
|
||||
[contributors-url]: https://github.com/OpenDevin/OpenDevin/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge
|
||||
[forks-url]: https://github.com/OpenDevin/OpenDevin/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge
|
||||
[stars-url]: https://github.com/OpenDevin/OpenDevin/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge
|
||||
[issues-url]: https://github.com/OpenDevin/OpenDevin/issues
|
||||
[license-shield]: https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge
|
||||
[license-url]: https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE
|
||||
@@ -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 generate the diagram to the diagram footer.
|
||||
4. Add the commit hash of the commit that was used to generat 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/))
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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"
|
||||
```
|
||||
@@ -1,96 +0,0 @@
|
||||
# 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,10 +60,30 @@ sudo systemctl stop ollama
|
||||
|
||||
For more info go [here](https://github.com/ollama/ollama/blob/main/docs/faq.md)
|
||||
|
||||
## 3. Start OpenDevin
|
||||
## 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
|
||||
```
|
||||
|
||||
Use the instructions in [README.md](/README.md) to start OpenDevin using Docker.
|
||||
When running `docker run`, add the following environment variables using `-e`:
|
||||
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:
|
||||
|
||||
```
|
||||
LLM_API_KEY="ollama"
|
||||
@@ -72,37 +92,20 @@ 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"`
|
||||
|
||||
You should now be able to connect to `http://localhost:3001/` with your local model running!
|
||||
## 6. Start OpenDevin:
|
||||
|
||||
|
||||
## 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.
|
||||
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!
|
||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
+2
-49
@@ -17,63 +17,16 @@ all the preprocessing/evaluation/analysis scripts.
|
||||
- Ensure compatibility with OpenAI interface for inference.
|
||||
- Serve open source models, prioritizing high concurrency and throughput.
|
||||
|
||||
## SWE-bench
|
||||
## Tasks
|
||||
### 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 |
|
||||
@@ -29,10 +29,6 @@ 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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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}
|
||||
@@ -1,155 +0,0 @@
|
||||
# @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}'
|
||||
}
|
||||
@@ -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-3.5-turbo-1106
|
||||
python evaluation/regression/run_tests.py --OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx --model=gpt-4-0125-preview
|
||||
```
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -67,9 +67,9 @@ def model(request):
|
||||
request: The pytest request object.
|
||||
|
||||
Returns:
|
||||
The model name, defaulting to "gpt-3.5-turbo-1106".
|
||||
""
|
||||
return request.config.getoption("model", default="gpt-3.5-turbo-1106")
|
||||
The model name, defaulting to "gpt-4-0125-preview".
|
||||
"""
|
||||
return request.config.getoption("model", default="gpt-4-0125-preview")
|
||||
|
||||
@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-3.5-turbo-1106"], 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-4-0125-preview"], 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()
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -1,3 +1,2 @@
|
||||
#!/bin/sh
|
||||
cd frontend
|
||||
npx lint-staged
|
||||
Generated
+2627
-4029
File diff suppressed because it is too large
Load Diff
+23
-21
@@ -11,25 +11,28 @@
|
||||
"@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",
|
||||
@@ -38,10 +41,10 @@
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test": "vitest",
|
||||
"test": "jest",
|
||||
"preview": "vite preview",
|
||||
"make-i18n": "node scripts/make-i18n-translations.cjs",
|
||||
"prelint": "npm run make-i18n",
|
||||
"prelint": "pnpm run make-i18n",
|
||||
"lint": "eslint src/**/*.ts* && prettier --check src/**/*.ts*",
|
||||
"prepare": "cd .. && husky install frontend/.husky"
|
||||
},
|
||||
@@ -56,20 +59,19 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest/presets/js-with-ts",
|
||||
"testEnvironment": "jest-environment-jsdom",
|
||||
"modulePaths": [
|
||||
"<rootDir>/src"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@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",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@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",
|
||||
@@ -77,15 +79,15 @@
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.2",
|
||||
"typescript": "^5.4.3",
|
||||
"vitest": "^1.5.0"
|
||||
"ts-jest": "^29.1.2"
|
||||
},
|
||||
"packageManager": "npm@10.5.0",
|
||||
"packageManager": "pnpm@8.15.6",
|
||||
"volta": {
|
||||
"node": "18.20.1"
|
||||
}
|
||||
|
||||
Generated
+8596
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
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();
|
||||
});
|
||||
+16
-67
@@ -1,18 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { 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 { initializeAgent } from "./services/settingsService";
|
||||
import Socket from "./services/socket";
|
||||
import { ResFetchMsgTotal } from "./types/ResponseType";
|
||||
|
||||
interface Props {
|
||||
setSettingOpen: (isOpen: boolean) => void;
|
||||
@@ -20,7 +13,7 @@ interface Props {
|
||||
|
||||
function LeftNav({ setSettingOpen }: Props): JSX.Element {
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4 bg-neutral-900 w-16 items-center shrink-0">
|
||||
<div className="flex flex-col h-full p-4 bg-bg-dark w-16 items-center shrink-0">
|
||||
<div
|
||||
className="mt-auto cursor-pointer hover:opacity-80"
|
||||
onClick={() => setSettingOpen(true)}
|
||||
@@ -31,75 +24,31 @@ 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 [isWarned, setIsWarned] = useState(false);
|
||||
const [loadMsgWarning, setLoadMsgWarning] = useState(false);
|
||||
|
||||
const getMsgTotal = () => {
|
||||
if (isWarned) return;
|
||||
fetchMsgTotal()
|
||||
.then((data: ResFetchMsgTotal) => {
|
||||
if (data.msg_total > 0) {
|
||||
setLoadMsgWarning(true);
|
||||
setIsWarned(true);
|
||||
}
|
||||
})
|
||||
.catch();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initOnce) return;
|
||||
initOnce = true;
|
||||
|
||||
initializeAgent();
|
||||
|
||||
Socket.registerCallback("open", [getMsgTotal]);
|
||||
|
||||
getMsgTotal();
|
||||
}, []);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSettingOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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 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>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function ArrowIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArrowIcon;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function PauseIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 5.25v13.5m-7.5-13.5v13.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default PauseIcon;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function PlayIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayIcon;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function StopIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5.25 7.5A2.25 2.25 0 0 1 7.5 5.25h9a2.25 2.25 0 0 1 2.25 2.25v9a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-9Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default StopIcon;
|
||||
@@ -1,152 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Button, ButtonGroup, Tooltip } from "@nextui-org/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import PauseIcon from "../assets/pause";
|
||||
import PlayIcon from "../assets/play";
|
||||
import AgentTaskAction from "../types/AgentTaskAction";
|
||||
import { changeTaskState } from "../services/agentStateService";
|
||||
import store, { RootState } from "../store";
|
||||
import AgentTaskState from "../types/AgentTaskState";
|
||||
import ArrowIcon from "../assets/arrow";
|
||||
import { clearMsgs } from "../services/session";
|
||||
import { clearMessages } from "../state/chatSlice";
|
||||
|
||||
const TaskStateActionMap = {
|
||||
[AgentTaskAction.START]: AgentTaskState.RUNNING,
|
||||
[AgentTaskAction.PAUSE]: AgentTaskState.PAUSED,
|
||||
[AgentTaskAction.RESUME]: AgentTaskState.RUNNING,
|
||||
[AgentTaskAction.STOP]: AgentTaskState.STOPPED,
|
||||
};
|
||||
|
||||
const IgnoreTaskStateMap: { [k: string]: AgentTaskState[] } = {
|
||||
[AgentTaskAction.PAUSE]: [
|
||||
AgentTaskState.INIT,
|
||||
AgentTaskState.PAUSED,
|
||||
AgentTaskState.STOPPED,
|
||||
AgentTaskState.FINISHED,
|
||||
],
|
||||
[AgentTaskAction.RESUME]: [
|
||||
AgentTaskState.INIT,
|
||||
AgentTaskState.RUNNING,
|
||||
AgentTaskState.STOPPED,
|
||||
AgentTaskState.FINISHED,
|
||||
],
|
||||
[AgentTaskAction.STOP]: [
|
||||
AgentTaskState.INIT,
|
||||
AgentTaskState.STOPPED,
|
||||
AgentTaskState.FINISHED,
|
||||
],
|
||||
};
|
||||
|
||||
interface ButtonProps {
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
content: string;
|
||||
action: AgentTaskAction;
|
||||
handleAction: (action: AgentTaskAction) => void;
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
isLoading = false,
|
||||
isDisabled = false,
|
||||
content,
|
||||
action,
|
||||
handleAction,
|
||||
children,
|
||||
}: React.PropsWithChildren<ButtonProps>): React.ReactNode {
|
||||
return (
|
||||
<Tooltip content={content} closeDelay={100}>
|
||||
<Button
|
||||
isIconOnly
|
||||
onClick={() => handleAction(action)}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentControlBar() {
|
||||
const { curTaskState } = useSelector((state: RootState) => state.agent);
|
||||
const [desiredState, setDesiredState] = React.useState(AgentTaskState.INIT);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const handleAction = (action: AgentTaskAction) => {
|
||||
if (IgnoreTaskStateMap[action].includes(curTaskState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let act = action;
|
||||
|
||||
if (act === AgentTaskAction.STOP) {
|
||||
act = AgentTaskAction.STOP;
|
||||
clearMsgs().then().catch();
|
||||
store.dispatch(clearMessages());
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
setDesiredState(TaskStateActionMap[act]);
|
||||
changeTaskState(act);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (curTaskState === desiredState) {
|
||||
if (curTaskState === AgentTaskState.STOPPED) {
|
||||
clearMsgs().then().catch();
|
||||
store.dispatch(clearMessages());
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (curTaskState === AgentTaskState.RUNNING) {
|
||||
setDesiredState(AgentTaskState.RUNNING);
|
||||
}
|
||||
}, [curTaskState]);
|
||||
|
||||
return (
|
||||
<div className="ml-5 mt-3">
|
||||
<ButtonGroup size="sm" variant="ghost">
|
||||
<ActionButton
|
||||
isLoading={false}
|
||||
isDisabled={isLoading}
|
||||
content="Restart a new agent task"
|
||||
action={AgentTaskAction.STOP}
|
||||
handleAction={handleAction}
|
||||
>
|
||||
<ArrowIcon />
|
||||
</ActionButton>
|
||||
|
||||
{curTaskState === AgentTaskState.PAUSED ? (
|
||||
<ActionButton
|
||||
isLoading={isLoading}
|
||||
isDisabled={
|
||||
isLoading ||
|
||||
IgnoreTaskStateMap[AgentTaskAction.RESUME].includes(curTaskState)
|
||||
}
|
||||
content="Resume the agent task"
|
||||
action={AgentTaskAction.RESUME}
|
||||
handleAction={handleAction}
|
||||
>
|
||||
<PlayIcon />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
isLoading={isLoading}
|
||||
isDisabled={
|
||||
isLoading ||
|
||||
IgnoreTaskStateMap[AgentTaskAction.PAUSE].includes(curTaskState)
|
||||
}
|
||||
content="Pause the agent task"
|
||||
action={AgentTaskAction.PAUSE}
|
||||
handleAction={handleAction}
|
||||
>
|
||||
<PauseIcon />
|
||||
</ActionButton>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgentControlBar;
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
import { RootState } from "../store";
|
||||
import AgentTaskState from "../types/AgentTaskState";
|
||||
|
||||
const AgentStatusMap: { [k: string]: { message: string; indicator: string } } =
|
||||
{
|
||||
[AgentTaskState.INIT]: {
|
||||
message: "Agent is initialized, waiting for task...",
|
||||
indicator: "bg-blue-500",
|
||||
},
|
||||
[AgentTaskState.RUNNING]: {
|
||||
message: "Agent is running task...",
|
||||
indicator: "bg-green-500",
|
||||
},
|
||||
[AgentTaskState.PAUSED]: {
|
||||
message: "Agent has paused.",
|
||||
indicator: "bg-yellow-500",
|
||||
},
|
||||
[AgentTaskState.STOPPED]: {
|
||||
message: "Agent has stopped.",
|
||||
indicator: "bg-red-500",
|
||||
},
|
||||
[AgentTaskState.FINISHED]: {
|
||||
message: "Agent has finished the task.",
|
||||
indicator: "bg-green-500",
|
||||
},
|
||||
};
|
||||
|
||||
function AgentStatusBar() {
|
||||
const { t } = useTranslation();
|
||||
const { initialized } = useSelector((state: RootState) => state.task);
|
||||
const { curTaskState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
// 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">
|
||||
{initialized ? (
|
||||
<>
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full animate-pulse ${AgentStatusMap[curTaskState].indicator}`}
|
||||
/>
|
||||
<span className="text-sm text-stone-400">
|
||||
{AgentStatusMap[curTaskState].message}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-3 h-3 bg-orange-800 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,33 +0,0 @@
|
||||
import React from "react";
|
||||
import Browser from "./Browser";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
describe("Browser", () => {
|
||||
it("renders a message if no screenshotSrc is provided", () => {
|
||||
const { getByText } = renderWithProviders(<Browser />, {
|
||||
preloadedState: {
|
||||
browser: {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText(/no screenshot available/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the url and a screenshot", () => {
|
||||
const { getByText, getByAltText } = renderWithProviders(<Browser />, {
|
||||
preloadedState: {
|
||||
browser: {
|
||||
url: "https://example.com",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText("https://example.com")).toBeInTheDocument();
|
||||
expect(getByAltText(/browser screenshot/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ function Browser(): JSX.Element {
|
||||
: `data:image/png;base64,${screenshotSrc || ""}`;
|
||||
|
||||
return (
|
||||
<div className="h-full m-2 bg-neutral-700 mockup-browser">
|
||||
<div className="h-full m-2 bg-bg-workspace mockup-browser">
|
||||
<div className="mockup-browser-toolbar">
|
||||
<div className="input">{url}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Card, CardBody } from "@nextui-org/react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { IoMdChatbubbles } from "react-icons/io";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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,
|
||||
setTypingActive,
|
||||
takeOneAndType,
|
||||
setCurrentQueueMarkerState,
|
||||
setCurrentTypingMsgState,
|
||||
setTypingAcitve,
|
||||
} from "../services/chatService";
|
||||
import { Message } from "../state/chatSlice";
|
||||
import { RootState } from "../store";
|
||||
import AgentStatusBar from "./AgentStatusBar";
|
||||
import Input from "./Input";
|
||||
import AgentControlBar from "./AgentControlBar";
|
||||
|
||||
interface IChatBubbleProps {
|
||||
msg: Message;
|
||||
@@ -27,35 +29,40 @@ interface IChatBubbleProps {
|
||||
*
|
||||
*/
|
||||
function TypingChat() {
|
||||
const { typeThis } = useSelector((state: RootState) => state.chat);
|
||||
const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
|
||||
useSelector((state: RootState) => state.chat);
|
||||
|
||||
const messageContent = useTypingEffect([typeThis?.content], {
|
||||
const messageContent = useTypingEffect([currentTypingMessage], {
|
||||
loop: false,
|
||||
setTypingActive,
|
||||
playbackRate: 0.099,
|
||||
setTypingAcitve,
|
||||
setCurrentQueueMarkerState,
|
||||
currentQueueMarker,
|
||||
playbackRate: 0.1,
|
||||
addAssistantMessageToChat,
|
||||
takeOneAndType,
|
||||
typeThis,
|
||||
assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="bg-neutral-500">
|
||||
<CardBody>{messageContent}</CardBody>
|
||||
</Card>
|
||||
currentQueueMarker !== null && (
|
||||
<Card className="bg-success-100">
|
||||
<CardBody>{messageContent}</CardBody>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={`flex mb-2.5 pr-5 pl-5 max-w-[90%] ${msg?.sender === "user" ? "self-end" : ""}`}
|
||||
>
|
||||
<div className="flex mb-2.5 pr-5 pl-5">
|
||||
<div
|
||||
className={`flex mt-2.5 mb-0 min-w-0 ${msg?.sender === "user" && "flex-row-reverse ml-auto"}`}
|
||||
>
|
||||
<Card
|
||||
className={`${msg?.sender === "user" ? "bg-neutral-700" : "bg-neutral-500"}`}
|
||||
>
|
||||
<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" : ""}`}>
|
||||
<CardBody>{msg?.content}</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -65,9 +72,14 @@ function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
|
||||
|
||||
function MessageList(): JSX.Element {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { typingActive, newChatSequence, typeThis } = useSelector(
|
||||
(state: RootState) => state.chat,
|
||||
);
|
||||
const {
|
||||
messages,
|
||||
queuedTyping,
|
||||
typingActive,
|
||||
currentQueueMarker,
|
||||
currentTypingMessage,
|
||||
newChatSequence,
|
||||
} = useSelector((state: RootState) => state.chat);
|
||||
|
||||
const messageScroll = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
@@ -89,21 +101,65 @@ function MessageList(): JSX.Element {
|
||||
}, [newChatSequence, typingActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeThis.content === "") return;
|
||||
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
|
||||
|
||||
if (!typingActive) setTypingActive(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typeThis]);
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto flex flex-col">
|
||||
{newChatSequence.map((msg, index) => (
|
||||
<ChatBubble key={index} msg={msg} />
|
||||
))}
|
||||
<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} />
|
||||
),
|
||||
)}
|
||||
|
||||
{typingActive && (
|
||||
<div className="flex mb-2.5 pr-5 pl-5 max-w-[90%]">
|
||||
<div className="flex mb-2.5 pr-5 pl-5 bg-s">
|
||||
<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>
|
||||
@@ -113,16 +169,28 @@ function MessageList(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatInterface(): JSX.Element {
|
||||
function InitializingStatus(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<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 />
|
||||
<AgentStatusBar />
|
||||
<AgentControlBar />
|
||||
<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 />}
|
||||
<Input />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,77 +1,44 @@
|
||||
import React from "react";
|
||||
import Editor, { Monaco } from "@monaco-editor/react";
|
||||
import { Tab, Tabs } from "@nextui-org/react";
|
||||
import type { editor } from "monaco-editor";
|
||||
import React, { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { editor } from "monaco-editor";
|
||||
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": "#171717",
|
||||
"editor.background": bgColor,
|
||||
},
|
||||
});
|
||||
|
||||
// 应用自定义主题 - English: apply custom theme
|
||||
// 应用自定义主题
|
||||
monaco.editor.setTheme("my-theme");
|
||||
};
|
||||
|
||||
return (
|
||||
<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="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 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%"
|
||||
path={
|
||||
selectedFileName === ""
|
||||
? "welcome.txt"
|
||||
: selectedFileName.toLocaleLowerCase()
|
||||
}
|
||||
defaultValue="# Welcome to OpenDevin!"
|
||||
value={code}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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="fixed left-1/2 transform -translate-x-1/2 top-4 z-50">
|
||||
<div className="errors">
|
||||
{errors.map((error, index) => (
|
||||
<div key={index} className="bg-red-800 p-4 rounded-md shadow-md mb-2">
|
||||
<div key={index} className="error">
|
||||
ERROR: {error}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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;
|
||||
@@ -1,227 +0,0 @@
|
||||
import { Accordion, AccordionItem } from "@nextui-org/react";
|
||||
import React, { useEffect } from "react";
|
||||
import TreeView, {
|
||||
ITreeViewOnExpandProps,
|
||||
ITreeViewOnNodeSelectProps,
|
||||
} 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 { getWorkspaceDepthOne, selectFile } from "../services/fileService";
|
||||
import {
|
||||
pruneWorkspace,
|
||||
resetWorkspace,
|
||||
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 workspaceTree = useSelector(
|
||||
(state: RootState) => state.code.workspaceFolder,
|
||||
);
|
||||
|
||||
const selectedIds = useSelector((state: RootState) => state.code.selectedIds);
|
||||
|
||||
useEffect(() => {
|
||||
getWorkspaceDepthOne("").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.element.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));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeExpand = (node: ITreeViewOnExpandProps) => {
|
||||
if (node.isExpanded) {
|
||||
const currentNode = workspaceTree.find(
|
||||
(treeNode) => treeNode.id === node.element.id,
|
||||
);
|
||||
if (!currentNode) return;
|
||||
getWorkspaceDepthOne(currentNode.relativePath).then((files) => {
|
||||
dispatch(updateWorkspace(files));
|
||||
});
|
||||
} else {
|
||||
const currentNode = workspaceTree.find(
|
||||
(treeNode) => treeNode.id === node.element.id,
|
||||
);
|
||||
dispatch(pruneWorkspace(currentNode));
|
||||
}
|
||||
};
|
||||
|
||||
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=""
|
||||
title={
|
||||
<div className="group flex items-center justify-between">
|
||||
<span className="text-neutral-400 text-sm" />
|
||||
</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
|
||||
.filter((node) => node.children.length > 0)
|
||||
.map((node) => node.id)}
|
||||
onNodeSelect={handleNodeSelect}
|
||||
onExpand={handleNodeExpand}
|
||||
// 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={() => {
|
||||
dispatch(resetWorkspace());
|
||||
getWorkspaceDepthOne("").then((file) =>
|
||||
dispatch(updateWorkspace(file)),
|
||||
);
|
||||
}}
|
||||
ariaLabel="Refresh"
|
||||
/>
|
||||
<CloseButton
|
||||
onClick={() => setExplorerOpen(false)}
|
||||
ariaLabel="Close Explorer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Files;
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Textarea } from "@nextui-org/react";
|
||||
import React, { ChangeEvent, KeyboardEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { VscSend } from "react-icons/vsc";
|
||||
import React, { ChangeEvent, useState, KeyboardEvent } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Textarea } from "@nextui-org/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import useInputComposition from "../hooks/useInputComposition";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
import { sendChatMessage } from "../services/chatService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RootState } from "../store";
|
||||
import useInputComposition from "../hooks/useInputComposition";
|
||||
import { sendChatMessage } from "../services/chatService";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
|
||||
function Input() {
|
||||
const { t } = useTranslation();
|
||||
@@ -37,6 +36,7 @@ function Input() {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
@@ -44,9 +44,9 @@ function Input() {
|
||||
return (
|
||||
<div className="w-full relative text-base">
|
||||
<Textarea
|
||||
className="pt-2 pb-4 px-4"
|
||||
disabled={!initialized}
|
||||
className="py-4 px-4"
|
||||
classNames={{
|
||||
inputWrapper: "bg-neutral-700",
|
||||
input: "pr-16 py-2",
|
||||
}}
|
||||
value={inputMessage}
|
||||
@@ -67,9 +67,8 @@ function Input() {
|
||||
)}
|
||||
onClick={handleSendMessage}
|
||||
disabled={!initialized}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<VscSend />
|
||||
{t(I18nKey.CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
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;
|
||||
@@ -1,71 +0,0 @@
|
||||
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,7 +1,37 @@
|
||||
import React from "react";
|
||||
|
||||
function Planner(): JSX.Element {
|
||||
return <div className="h-full w-full bg-neutral-700">Coming soon...</div>;
|
||||
return (
|
||||
<div className="h-full w-full bg-bg-workspace">
|
||||
<h3>
|
||||
Current Focus: Set up the development environment according to the
|
||||
project'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's
|
||||
instructions.
|
||||
</span>
|
||||
</li>
|
||||
{/* Add more tasks */}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Planner;
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +1,104 @@
|
||||
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 {
|
||||
fetchAgents,
|
||||
INITIAL_AGENTS,
|
||||
fetchModels,
|
||||
saveSettings,
|
||||
getCurrentSettings,
|
||||
Settings,
|
||||
fetchAgents,
|
||||
INITIAL_MODELS,
|
||||
sendSettings,
|
||||
getInitialModel,
|
||||
} from "../services/settingsService";
|
||||
import {
|
||||
setModel,
|
||||
setAgent,
|
||||
setWorkspaceDirectory,
|
||||
setLanguage,
|
||||
} from "../state/settingsSlice";
|
||||
import store, { RootState } from "../store";
|
||||
import socket from "../socket/socket";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
import { AvailableLanguages } from "../i18n";
|
||||
import { ArgConfigType } from "../types/ConfigType";
|
||||
import ODModal from "./ODModal";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
|
||||
const currentSettings: Settings = getCurrentSettings();
|
||||
const [model, setModel] = useState(currentSettings[ArgConfigType.LLM_MODEL]);
|
||||
const [inputModel, setInputModel] = useState(
|
||||
currentSettings[ArgConfigType.LLM_MODEL],
|
||||
);
|
||||
const [agent, setAgent] = useState(currentSettings[ArgConfigType.AGENT]);
|
||||
const [language, setLanguage] = useState(
|
||||
currentSettings[ArgConfigType.LANGUAGE],
|
||||
);
|
||||
const cachedModels = JSON.parse(
|
||||
localStorage.getItem("supportedModels") || "[]",
|
||||
);
|
||||
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,
|
||||
);
|
||||
const language = useSelector((state: RootState) => state.settings.language);
|
||||
|
||||
const [supportedModels, setSupportedModels] = useState([]);
|
||||
const [supportedAgents, setSupportedAgents] = useState([]);
|
||||
const [supportedModels, setSupportedModels] = useState(
|
||||
cachedModels.length > 0 ? cachedModels : INITIAL_MODELS,
|
||||
);
|
||||
const [supportedAgents, setSupportedAgents] = useState(
|
||||
cachedAgents.length > 0 ? cachedAgents : INITIAL_AGENTS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels().then((fetchedModels) => {
|
||||
const sortedModels = fetchedModels.sort(); // Sorting the models alphabetically
|
||||
setSupportedModels(sortedModels);
|
||||
});
|
||||
async function setInitialModel() {
|
||||
const initialModel = await getInitialModel();
|
||||
store.dispatch(setModel(initialModel));
|
||||
}
|
||||
setInitialModel();
|
||||
|
||||
fetchModels().then((fetchedModels) => {
|
||||
setSupportedModels(fetchedModels);
|
||||
localStorage.setItem("supportedModels", JSON.stringify(fetchedModels));
|
||||
});
|
||||
fetchAgents().then((fetchedAgents) => {
|
||||
setSupportedAgents(fetchedAgents);
|
||||
localStorage.setItem("supportedAgents", JSON.stringify(fetchedAgents));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSaveCfg = () => {
|
||||
saveSettings({
|
||||
[ArgConfigType.LLM_MODEL]: model ?? inputModel,
|
||||
[ArgConfigType.AGENT]: agent,
|
||||
[ArgConfigType.LANGUAGE]: language,
|
||||
});
|
||||
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);
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -65,90 +106,96 @@ function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
|
||||
item.toLowerCase().includes(input.toLowerCase());
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
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 "../services/socket";
|
||||
import { RootState } from "../store";
|
||||
import ActionType from "../types/ActionType";
|
||||
import ObservationType from "../types/ObservationType";
|
||||
import socket from "../socket/socket";
|
||||
|
||||
class JsonWebsocketAddon {
|
||||
_socket: WebSocket;
|
||||
|
||||
_disposables: IDisposable[];
|
||||
|
||||
constructor() {
|
||||
constructor(_socket: WebSocket) {
|
||||
this._socket = _socket;
|
||||
this._disposables = [];
|
||||
}
|
||||
|
||||
@@ -20,15 +18,15 @@ class JsonWebsocketAddon {
|
||||
this._disposables.push(
|
||||
terminal.onData((data) => {
|
||||
const payload = JSON.stringify({ action: "terminal", data });
|
||||
Socket.send(payload);
|
||||
this._socket.send(payload);
|
||||
}),
|
||||
);
|
||||
Socket.addEventListener("message", (event) => {
|
||||
this._socket.addEventListener("message", (event) => {
|
||||
const { action, args, observation, content } = JSON.parse(event.data);
|
||||
if (action === ActionType.RUN) {
|
||||
if (action === "run") {
|
||||
terminal.writeln(args.command);
|
||||
}
|
||||
if (observation === ObservationType.RUN) {
|
||||
if (observation === "run") {
|
||||
content.split("\n").forEach((line: string) => {
|
||||
terminal.writeln(line);
|
||||
});
|
||||
@@ -39,7 +37,7 @@ class JsonWebsocketAddon {
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach((d) => d.dispose());
|
||||
Socket.removeEventListener("message", () => {});
|
||||
this._socket.removeEventListener("message", () => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +48,12 @@ 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.
|
||||
@@ -63,7 +64,7 @@ function Terminal(): JSX.Element {
|
||||
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
||||
fontSize: 14,
|
||||
theme: {
|
||||
background: "#262626",
|
||||
background: bgColor,
|
||||
},
|
||||
});
|
||||
terminal.write("$ ");
|
||||
@@ -79,32 +80,17 @@ function Terminal(): JSX.Element {
|
||||
fitAddon.fit();
|
||||
}, 1);
|
||||
|
||||
const jsonWebsocketAddon = new JsonWebsocketAddon();
|
||||
const jsonWebsocketAddon = new JsonWebsocketAddon(socket);
|
||||
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="flex items-center gap-2 px-4 py-2 text-sm border-b border-neutral-600">
|
||||
<VscTerminal />
|
||||
Terminal
|
||||
</div>
|
||||
<div className="px-4 py-2 text-lg border-b border-border">Terminal</div>
|
||||
<div className="grow p-2 flex min-h-0">
|
||||
<div ref={terminalRef} className="h-full w-full" />
|
||||
</div>
|
||||
|
||||
@@ -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: <VscCode size={18} />,
|
||||
icon: <Pencil />,
|
||||
component: <CodeEditor key="code" />,
|
||||
},
|
||||
[TabOption.BROWSER]: {
|
||||
name: t(I18nKey.WORKSPACE$BROWSER_TAB_LABEL),
|
||||
icon: <IoIosGlobe size={18} />,
|
||||
icon: <Earth />,
|
||||
component: <Browser key="browser" />,
|
||||
},
|
||||
}),
|
||||
@@ -36,31 +36,23 @@ function Workspace() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-0 grow">
|
||||
<>
|
||||
<div
|
||||
role="tablist"
|
||||
className="tabs tabs-bordered tabs-lg border-b border-neutral-600 flex"
|
||||
className="tabs tabs-bordered tabs-lg border-b border-border"
|
||||
>
|
||||
<Tabs
|
||||
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",
|
||||
}}
|
||||
variant="light"
|
||||
size="lg"
|
||||
onSelectionChange={(v) => {
|
||||
setActiveTab(v as TabType);
|
||||
}}
|
||||
>
|
||||
{AllTabs.map((tab, index) => (
|
||||
{AllTabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={`flex-grow ${index + 1 === AllTabs.length ? "" : "border-r"}`}
|
||||
title={
|
||||
<div className="flex grow items-center gap-2 justify-center text-xs">
|
||||
<div className="flex items-center space-x-2">
|
||||
{tabData[tab].icon}
|
||||
<span>{tabData[tab].name}</span>
|
||||
</div>
|
||||
@@ -69,10 +61,16 @@ function Workspace() {
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="grow w-full bg-neutral-800 flex min-h-0">
|
||||
{tabData[activeTab as TabType].component}
|
||||
</div>
|
||||
</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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Workspace;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,156 +0,0 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useTypingEffect } from "./useTypingEffect";
|
||||
|
||||
describe("useTypingEffect", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
// 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(() => {
|
||||
vi.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
|
||||
});
|
||||
|
||||
expect(result.current).toBe(message.slice(0, -1));
|
||||
|
||||
act(() => {
|
||||
vi.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(() => {
|
||||
vi.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
|
||||
});
|
||||
|
||||
expect(result.current).toBe(message.slice(0, -1));
|
||||
|
||||
act(() => {
|
||||
vi.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(() => {
|
||||
vi.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
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current).toBe(messages[1]); // World
|
||||
});
|
||||
|
||||
it("should call setTypingActive with false when typing completes without loop", () => {
|
||||
const setTypingActiveMock = vi.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useTypingEffect(["Hello, world!", "This is a test message."], {
|
||||
loop: false,
|
||||
setTypingActive: setTypingActiveMock,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(setTypingActiveMock).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(setTypingActiveMock).toHaveBeenCalledWith(false);
|
||||
expect(setTypingActiveMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call addAssistantMessageToChat with the typeThis argument when typing completes without loop", () => {
|
||||
const addAssistantMessageToChatMock = vi.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(() => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(addAssistantMessageToChatMock).toHaveBeenCalledTimes(1);
|
||||
expect(addAssistantMessageToChatMock).toHaveBeenCalledWith({
|
||||
content: "Hello, world!",
|
||||
sender: "assistant",
|
||||
});
|
||||
});
|
||||
|
||||
it("should call takeOneAndType when typing completes without loop", () => {
|
||||
const takeOneAndTypeMock = vi.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useTypingEffect(["Hello, world!", "This is a test message."], {
|
||||
loop: false,
|
||||
takeOneAndType: takeOneAndTypeMock,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(takeOneAndTypeMock).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(takeOneAndTypeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Implementation is not clear on how to handle this case
|
||||
it.todo("should handle typing with loop");
|
||||
});
|
||||
@@ -8,24 +8,26 @@ export const useTypingEffect = (
|
||||
{
|
||||
loop = false,
|
||||
playbackRate = 0.1,
|
||||
setTypingActive = () => {},
|
||||
setTypingAcitve = () => {},
|
||||
setCurrentQueueMarkerState = () => {},
|
||||
currentQueueMarker = 0,
|
||||
addAssistantMessageToChat = () => {},
|
||||
takeOneAndType = () => {},
|
||||
typeThis = { content: "", sender: "assistant" },
|
||||
assistantMessageObj = { content: "", sender: "assistant" },
|
||||
}: {
|
||||
loop?: boolean;
|
||||
playbackRate?: number;
|
||||
setTypingActive?: (bool: boolean) => void;
|
||||
setTypingAcitve?: (bool: boolean) => void;
|
||||
setCurrentQueueMarkerState?: (marker: number) => void;
|
||||
currentQueueMarker?: number;
|
||||
addAssistantMessageToChat?: (msg: Message) => void;
|
||||
takeOneAndType?: () => void;
|
||||
typeThis?: Message;
|
||||
assistantMessageObj?: Message;
|
||||
} = {
|
||||
loop: false,
|
||||
playbackRate: 0.1,
|
||||
setTypingActive: () => {},
|
||||
setTypingAcitve: () => {},
|
||||
currentQueueMarker: 0,
|
||||
addAssistantMessageToChat: () => {},
|
||||
takeOneAndType: () => {},
|
||||
typeThis: { content: "", sender: "assistant" },
|
||||
assistantMessageObj: { content: "", sender: "assistant" },
|
||||
},
|
||||
) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
@@ -47,9 +49,9 @@ export const useTypingEffect = (
|
||||
stringIndex++;
|
||||
if (stringIndex === strings.length) {
|
||||
if (!loop) {
|
||||
setTypingActive(false);
|
||||
addAssistantMessageToChat(typeThis);
|
||||
takeOneAndType();
|
||||
setTypingAcitve(false);
|
||||
setCurrentQueueMarkerState(currentQueueMarker + 1);
|
||||
addAssistantMessageToChat(assistantMessageObj);
|
||||
return;
|
||||
}
|
||||
stringIndex = 0;
|
||||
@@ -71,7 +73,6 @@ export const useTypingEffect = (
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const nonBreakingSpace = "\u00A0";
|
||||
|
||||
@@ -2,21 +2,10 @@ 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: "Arabic", value: "ar" },
|
||||
{ label: "Deutsch", value: "de" },
|
||||
{ label: "Français", value: "fr" },
|
||||
{ label: "Italiano", value: "it" },
|
||||
{ label: "Português", value: "pt" },
|
||||
{ label: "Español", value: "es" },
|
||||
{ label: "Türkçe", value: "tr" },
|
||||
];
|
||||
|
||||
i18n
|
||||
@@ -28,17 +17,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(ArgConfigType.LANGUAGE);
|
||||
const settingLanguage = localStorage.getItem("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(ArgConfigType.LANGUAGE, detectLanguage);
|
||||
localStorage.setItem("language", detectLanguage);
|
||||
i18n.changeLanguage(detectLanguage);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,286 +1,74 @@
|
||||
{
|
||||
"WORKSPACE$TITLE": {
|
||||
"en": "OpenDevin Workspace",
|
||||
"zh-CN": "OpenDevin 工作区",
|
||||
"de": "OpenDevin Arbeitsbereich",
|
||||
"ko-KR": "OpenDevin 워크스페이스",
|
||||
"no": "OpenDevin Arbeidsområde",
|
||||
"zh-TW": "OpenDevin 工作區",
|
||||
"ar": "مساحة عمل أوبنديفين",
|
||||
"fr": "OpenDevin Espace de travail",
|
||||
"it": "Area di lavoro OpenDevin",
|
||||
"pt": "Espaço de trabalho OpenDevin",
|
||||
"es": "Espacio de trabajo de OpenDevin"
|
||||
"zh-CN": "OpenDevin 工作区"
|
||||
},
|
||||
"WORKSPACE$TERMINAL_TAB_LABEL": {
|
||||
"en": "Terminal",
|
||||
"zh-CN": "终端",
|
||||
"de": "Terminal",
|
||||
"ko-KR": "터미널",
|
||||
"no": "Terminal",
|
||||
"zh-TW": "終端機",
|
||||
"it": "Terminale",
|
||||
"pt": "Terminal",
|
||||
"es": "Terminal",
|
||||
"ar": "الطرفية",
|
||||
"fr": "Terminal",
|
||||
"tr": "Terminal"
|
||||
"zh-CN": "终端"
|
||||
},
|
||||
"WORKSPACE$PLANNER_TAB_LABEL": {
|
||||
"en": "Planner",
|
||||
"zh-CN": "规划器",
|
||||
"de": "Planer",
|
||||
"ko-KR": "플래너",
|
||||
"no": "Planlegger",
|
||||
"zh-TW": "計畫器",
|
||||
"ar": "المخطط",
|
||||
"fr": "Planificateur",
|
||||
"it": "Pianificatore",
|
||||
"pt": "Planejador",
|
||||
"es": "Planificador"
|
||||
"zh-CN": "规划器"
|
||||
},
|
||||
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
|
||||
"en": "Code Editor",
|
||||
"zh-CN": "代码编辑器",
|
||||
"de": "Code-Editor",
|
||||
"ko-KR": "코드 편집기",
|
||||
"no": "Kode editor",
|
||||
"zh-TW": "程式碼編輯器",
|
||||
"ar": "محرر الكود",
|
||||
"fr": "Éditeur de code",
|
||||
"it": "Editor di codice",
|
||||
"pt": "Editor de código",
|
||||
"es": "Editor de código"
|
||||
"zh-CN": "代码编辑器"
|
||||
},
|
||||
"WORKSPACE$BROWSER_TAB_LABEL": {
|
||||
"en": "Browser",
|
||||
"zh-CN": "浏览器",
|
||||
"de": "Browser",
|
||||
"ko-KR": "브라우저",
|
||||
"no": "Nettleser",
|
||||
"zh-TW": "瀏覽器",
|
||||
"it": "Browser",
|
||||
"pt": "Navegador",
|
||||
"es": "Navegador",
|
||||
"ar": "المتصفح",
|
||||
"fr": "Navigateur",
|
||||
"tr": "Tarayıcı"
|
||||
"zh-CN": "浏览器"
|
||||
},
|
||||
"CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_LABEL": {
|
||||
"en": "OpenDevin Workspace directory",
|
||||
"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",
|
||||
"ar": "مجلد مساحة عمل أوبنديفين",
|
||||
"fr": "Répertoire de l'espace de travail OpenDevin",
|
||||
"tr": "OpenDevin çalışma alanı dizini"
|
||||
"zh-CN": "OpenDevin 工作区目录"
|
||||
},
|
||||
"CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER": {
|
||||
"en": "Default: ./workspace",
|
||||
"zh-CN": "默认:./workspace",
|
||||
"de": "Standard: ./workspace",
|
||||
"ko-KR": "기본: ./workspace",
|
||||
"no": "Standard: ./workspace",
|
||||
"zh-TW": "默认:./workspace",
|
||||
"ar": "الافتراضي: ./workspace",
|
||||
"fr": "Par défaut: ./workspace",
|
||||
"it": "Predefinito: ./workspace",
|
||||
"pt": "Padrão: ./workspace",
|
||||
"es": "Predeterminado: ./workspace"
|
||||
"zh-CN": "默认:./workspace"
|
||||
},
|
||||
"CONFIGURATION$MODAL_TITLE": {
|
||||
"en": "Configuration",
|
||||
"zh-CN": "配置",
|
||||
"de": "Konfiguration",
|
||||
"ko-KR": "설정",
|
||||
"no": "Konfigurasjon",
|
||||
"zh-TW": "設定",
|
||||
"it": "Configurazione",
|
||||
"pt": "Configuração",
|
||||
"es": "Configuración",
|
||||
"ar": "التكوين",
|
||||
"fr": "Configuration",
|
||||
"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"
|
||||
"zh-CN": "配置"
|
||||
},
|
||||
"CONFIGURATION$MODEL_SELECT_LABEL": {
|
||||
"en": "Model",
|
||||
"zh-CN": "模型",
|
||||
"de": "Modell",
|
||||
"ko-KR": "모델",
|
||||
"no": "Modell",
|
||||
"zh-TW": "模型",
|
||||
"it": "Modello",
|
||||
"pt": "Modelo",
|
||||
"es": "Modelo",
|
||||
"ar": "النموذج",
|
||||
"fr": "Modèle",
|
||||
"tr": "Model"
|
||||
"zh-CN": "模型"
|
||||
},
|
||||
"CONFIGURATION$MODEL_SELECT_PLACEHOLDER": {
|
||||
"en": "Select a model",
|
||||
"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",
|
||||
"ar": "حدد نموذج",
|
||||
"fr": "Sélectionner un modèle",
|
||||
"tr": "Model Seç"
|
||||
"zh-CN": "选择一个模型"
|
||||
},
|
||||
"CONFIGURATION$AGENT_SELECT_LABEL": {
|
||||
"en": "Agent",
|
||||
"zh-CN": "智能体",
|
||||
"de": "Agent",
|
||||
"ko-KR": "에이전트",
|
||||
"no": "Agent",
|
||||
"zh-TW": "智能體",
|
||||
"it": "Agente",
|
||||
"pt": "Agente",
|
||||
"es": "Agente",
|
||||
"ar": "الوكيل",
|
||||
"fr": "Agent",
|
||||
"tr": "Ajan"
|
||||
"zh-CN": "代理"
|
||||
},
|
||||
"CONFIGURATION$AGENT_SELECT_PLACEHOLDER": {
|
||||
"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",
|
||||
"ar": "حدد وكيلا",
|
||||
"fr": "Sélectionner un agent",
|
||||
"tr": "Ajan Seç"
|
||||
"en": "Select a agent",
|
||||
"zh-CN": "选择一个代理"
|
||||
},
|
||||
"CONFIGURATION$LANGUAGE_SELECT_LABEL": {
|
||||
"en": "Language",
|
||||
"zh-CN": "语言",
|
||||
"de": "Sprache",
|
||||
"ko-KR": "언어",
|
||||
"no": "Språk",
|
||||
"zh-TW": "語言",
|
||||
"ar": "اللغة",
|
||||
"fr": "Langue",
|
||||
"it": "Lingua",
|
||||
"pt": "Idioma",
|
||||
"es": "Idioma"
|
||||
"zh-CN": "语言"
|
||||
},
|
||||
"CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL": {
|
||||
"en": "Close",
|
||||
"zh-CN": "关闭",
|
||||
"de": "Schließen",
|
||||
"ko-KR": "닫기",
|
||||
"no": "Lukk",
|
||||
"zh-TW": "關閉",
|
||||
"it": "Chiudi",
|
||||
"pt": "Fechar",
|
||||
"es": "Cerrar",
|
||||
"ar": "إغلاق",
|
||||
"fr": "Fermer",
|
||||
"tr": "Kapat"
|
||||
"zh-CN": "关闭"
|
||||
},
|
||||
"CONFIGURATION$MODAL_SAVE_BUTTON_LABEL": {
|
||||
"en": "Save",
|
||||
"zh-CN": "保存",
|
||||
"de": "Speichern",
|
||||
"ko-KR": "저장",
|
||||
"no": "Lagre",
|
||||
"zh-TW": "儲存",
|
||||
"ar": "حفظ",
|
||||
"fr": "Enregistrer",
|
||||
"it": "Salva",
|
||||
"pt": "Salvar",
|
||||
"es": "Guardar"
|
||||
"zh-CN": "保存"
|
||||
},
|
||||
"CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE": {
|
||||
"en": "Initializing agent (may take up to 10 seconds)...",
|
||||
"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)...",
|
||||
"ar": "جاري تهيئة الوكيل (قد يستغرق حتى 10 ثواني)...",
|
||||
"fr": "Initialisation de l'agent (peut prendre jusqu'à 10 secondes)...",
|
||||
"tr": "Ajan başlatılıyor (bu işlem 10 saniye kadar sürebilir)..."
|
||||
"zh-CN": "初始化代理(可能需要 10 秒以上时间)"
|
||||
},
|
||||
"CHAT_INTERFACE$INPUT_PLACEHOLDER": {
|
||||
"en": "Send a message (won't interrupt the Assistant)",
|
||||
"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)",
|
||||
"ar": "إرسال رسالة (لن يقاطع المساعد)",
|
||||
"fr": "Envoyer un message (ne pas interrompre l'Assistant)",
|
||||
"tr": "Bir mesaj gönderin (Asistan Kesilmeyecek)"
|
||||
"zh-CN": "发送消息(不会打断助理)"
|
||||
},
|
||||
"CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
|
||||
"en": "Send",
|
||||
"zh-CN": "发送",
|
||||
"de": "Senden",
|
||||
"ko-KR": "전송",
|
||||
"no": "Send",
|
||||
"zh-TW": "發送",
|
||||
"it": "Invia",
|
||||
"pt": "Enviar",
|
||||
"es": "Enviar",
|
||||
"ar": "إرسال",
|
||||
"fr": "Envoyer"
|
||||
},
|
||||
"CHAT_INTERFACE$INITIAL_MESSAGE": {
|
||||
"en": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"zh-CN": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"de": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"ko-KR": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"no": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"zh-TW": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"it": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"pt": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"es": "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?",
|
||||
"ar": "مرحبا! أنا OpenDevin، مهندس برمجيات AI. ماذا تود أن تبني معي اليوم؟",
|
||||
"fr": "Salut! Je suis OpenDevin, un ingénieur logiciel en IA. Que voudriez-vous construire avec moi aujourd'hui?"
|
||||
},
|
||||
"CHAT_INTERFACE$ASSISTANT": {
|
||||
"en": "Assistant",
|
||||
"zh-CN": "Assistant",
|
||||
"ko-KR": "Assistant",
|
||||
"de": "Assistant",
|
||||
"no": "Assistant",
|
||||
"zh-TW": "Assistant",
|
||||
"it": "Assistant",
|
||||
"pt": "Assistant",
|
||||
"es": "Assistant",
|
||||
"ar": "مساعد",
|
||||
"fr": "Assistant",
|
||||
"tr": "Gönder"
|
||||
"zh-CN": "发送"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-26
@@ -4,17 +4,12 @@
|
||||
--bg-input: #393939;
|
||||
--bg-workspace: #1f2228;
|
||||
--border: #3c3c4a;
|
||||
--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;
|
||||
background-color: var(--bg-dark) !important;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -25,22 +20,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { changeTaskState } from "../state/agentSlice";
|
||||
import { setScreenshotSrc, setUrl } from "../state/browserSlice";
|
||||
import { appendAssistantMessage } from "../state/chatSlice";
|
||||
import { setCode } from "../state/codeSlice";
|
||||
import { appendInput } from "../state/commandSlice";
|
||||
import { setInitialized } from "../state/taskSlice";
|
||||
import store from "../store";
|
||||
import ActionType from "../types/ActionType";
|
||||
import { ActionMessage } from "../types/Message";
|
||||
import { SocketMessage } from "../types/ResponseType";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
|
||||
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 { content } = message.args;
|
||||
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));
|
||||
},
|
||||
[ActionType.CHANGE_TASK_STATE]: (message: ActionMessage) => {
|
||||
store.dispatch(changeTaskState(message.args.task_state));
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import ActionType from "../types/ActionType";
|
||||
import Socket from "./socket";
|
||||
import AgentTaskAction from "../types/AgentTaskAction";
|
||||
|
||||
export function changeTaskState(message: AgentTaskAction): void {
|
||||
const eventString = JSON.stringify({
|
||||
action: ActionType.CHANGE_TASK_STATE,
|
||||
args: { task_state_action: message },
|
||||
});
|
||||
Socket.send(eventString);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import * as jose from "jose";
|
||||
import type { Mock } from "vitest";
|
||||
import { fetchToken, validateToken, getToken } from "./auth";
|
||||
|
||||
vi.mock("jose", () => ({
|
||||
decodeJwt: vi.fn(),
|
||||
}));
|
||||
|
||||
// SUGGESTION: Prefer using msw for mocking requests (see https://mswjs.io/)
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ token: "newToken" }),
|
||||
}),
|
||||
) as Mock;
|
||||
|
||||
Storage.prototype.getItem = vi.fn();
|
||||
Storage.prototype.setItem = vi.fn();
|
||||
|
||||
describe("Auth Service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
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 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 Mock).mockReturnValue({ sid: "123" });
|
||||
expect(validateToken("validToken")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for an invalid token", () => {
|
||||
(jose.decodeJwt as Mock).mockReturnValue({});
|
||||
expect(validateToken("invalidToken")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when decodeJwt throws", () => {
|
||||
(jose.decodeJwt as 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 Mock).mockReturnValue({ sid: "123" });
|
||||
(Storage.prototype.getItem as 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 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 Mock).mockReturnValue({});
|
||||
await expect(getToken()).rejects.toThrow("Token validation failed.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
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 };
|
||||
@@ -1,45 +1,36 @@
|
||||
import {
|
||||
Message,
|
||||
appendToNewChatSequence,
|
||||
appeendToNewChatSequence,
|
||||
appendUserMessage,
|
||||
takeOneTypeIt,
|
||||
emptyOutQueuedTyping,
|
||||
setCurrentQueueMarker,
|
||||
setCurrentTypingMessage,
|
||||
toggleTypingActive,
|
||||
} from "../state/chatSlice";
|
||||
import Socket from "./socket";
|
||||
import socket from "../socket/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: ActionType.START, args: { task: message } };
|
||||
const event = { action: "start", args: { task: message } };
|
||||
const eventString = JSON.stringify(event);
|
||||
Socket.send(eventString);
|
||||
socket.send(eventString);
|
||||
}
|
||||
|
||||
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 {
|
||||
export function setTypingAcitve(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(appendToNewChatSequence(msg));
|
||||
}
|
||||
export function takeOneAndType(): void {
|
||||
store.dispatch(takeOneTypeIt());
|
||||
store.dispatch(appeendToNewChatSequence(msg));
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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 type WorkspaceItem = {
|
||||
name: string;
|
||||
isBranch: boolean;
|
||||
relativePath: string;
|
||||
id: string;
|
||||
parent: string | null;
|
||||
children: string[];
|
||||
};
|
||||
|
||||
export async function getWorkspaceDepthOne(
|
||||
relpath: string,
|
||||
): Promise<WorkspaceItem[]> {
|
||||
const res = await fetch(`/api/list-files?relpath=${relpath}`);
|
||||
const json = await res.json();
|
||||
const files = json.files as WorkspaceItem[];
|
||||
return files;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import type { Mock } from "vitest";
|
||||
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 = vi.fn();
|
||||
Storage.prototype.getItem = vi.fn();
|
||||
|
||||
describe("Session Service", () => {
|
||||
beforeEach(() => {
|
||||
vi.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 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 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 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 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 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 Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ status: 401 }),
|
||||
);
|
||||
|
||||
await expect(clearMsgs()).rejects.toThrow("Delete messages failed.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
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 };
|
||||
@@ -1,68 +0,0 @@
|
||||
import { getUpdatedSettings } from "./settingsService";
|
||||
import { ArgConfigType } from "../types/ConfigType";
|
||||
|
||||
describe("mergeAndUpdateSettings", () => {
|
||||
it("should return initial settings if newSettings is empty", () => {
|
||||
const oldSettings = { key1: "value1" };
|
||||
|
||||
const result = getUpdatedSettings({}, oldSettings);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should add new keys to updatedSettings", () => {
|
||||
const oldSettings = { key1: "value1" };
|
||||
const newSettings = { key2: "value2" };
|
||||
|
||||
const result = getUpdatedSettings(newSettings, oldSettings);
|
||||
|
||||
expect(result).toEqual({
|
||||
key2: "value2", // New key
|
||||
});
|
||||
});
|
||||
|
||||
it("should overwrite non-DISPLAY_MAP keys in mergedSettings", () => {
|
||||
const oldSettings = { key1: "value1" };
|
||||
const newSettings = { key1: "newvalue1" };
|
||||
|
||||
const result = getUpdatedSettings(newSettings, oldSettings);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should show no values if they are equal", () => {
|
||||
const oldSettings = {
|
||||
[ArgConfigType.LLM_MODEL]: "gpt-4-0125-preview",
|
||||
[ArgConfigType.AGENT]: "MonologueAgent",
|
||||
};
|
||||
const newSettings = {
|
||||
[ArgConfigType.AGENT]: "MonologueAgent",
|
||||
};
|
||||
|
||||
const result = getUpdatedSettings(newSettings, oldSettings);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should update all settings", () => {
|
||||
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 result = getUpdatedSettings(newSettings, oldSettings);
|
||||
|
||||
expect(result).toEqual({
|
||||
[ArgConfigType.AGENT]: "CodeActAgent",
|
||||
[ArgConfigType.LANGUAGE]: "es",
|
||||
key2: "value2",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
import { appendAssistantMessage } from "../state/chatSlice";
|
||||
import { setInitialized } from "../state/taskSlice";
|
||||
import store from "../store";
|
||||
import ActionType from "../types/ActionType";
|
||||
import { SupportedSettings } from "../types/ConfigType";
|
||||
import Socket from "./socket";
|
||||
import { setByKey } from "../state/settingsSlice";
|
||||
import toast from "../utils/toast";
|
||||
|
||||
export type Settings = { [key: string]: string };
|
||||
export async function getInitialModel() {
|
||||
if (localStorage.getItem("model")) {
|
||||
return localStorage.getItem("model");
|
||||
}
|
||||
|
||||
const res = await fetch("/api/default-model");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchModels() {
|
||||
const response = await fetch(`/api/litellm-models`);
|
||||
@@ -14,78 +17,50 @@ export async function fetchModels() {
|
||||
}
|
||||
|
||||
export async function fetchAgents() {
|
||||
const response = await fetch(`/api/agents`);
|
||||
const response = await fetch(`/api/litellm-agents`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// all available settings in the frontend
|
||||
// TODO: add the values to i18n to support multi languages
|
||||
const DISPLAY_MAP: { [key: string]: string } = {
|
||||
LLM_MODEL: "model",
|
||||
AGENT: "agent",
|
||||
LANGUAGE: "language",
|
||||
};
|
||||
export const INITIAL_MODELS = [
|
||||
"gpt-3.5-turbo-1106",
|
||||
"gpt-4-0125-preview",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
];
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_MODEL: "gpt-3.5-turbo",
|
||||
AGENT: "MonologueAgent",
|
||||
LANGUAGE: "en",
|
||||
};
|
||||
export type Model = (typeof INITIAL_MODELS)[number];
|
||||
|
||||
const getSettingOrDefault = (key: string): string => {
|
||||
const value = localStorage.getItem(key);
|
||||
return value || DEFAULT_SETTINGS[key];
|
||||
};
|
||||
export const INITIAL_AGENTS = ["MonologueAgent", "CodeActAgent"];
|
||||
|
||||
export const getCurrentSettings = (): Settings => ({
|
||||
LLM_MODEL: getSettingOrDefault("LLM_MODEL"),
|
||||
AGENT: getSettingOrDefault("AGENT"),
|
||||
LANGUAGE: getSettingOrDefault("LANGUAGE"),
|
||||
});
|
||||
export type Agent = (typeof INITIAL_AGENTS)[number];
|
||||
|
||||
// Function to merge and update settings
|
||||
export const getUpdatedSettings = (
|
||||
newSettings: Settings,
|
||||
currentSettings: Settings,
|
||||
) => {
|
||||
const updatedSettings: Settings = {};
|
||||
SupportedSettings.forEach((setting) => {
|
||||
if (newSettings[setting] !== currentSettings[setting]) {
|
||||
updatedSettings[setting] = newSettings[setting];
|
||||
}
|
||||
});
|
||||
return updatedSettings;
|
||||
};
|
||||
// Map Redux settings to socket event arguments
|
||||
const SETTINGS_MAP = new Map<string, string>([
|
||||
["model", "model"],
|
||||
["agent", "agent_cls"],
|
||||
["workspaceDirectory", "directory"],
|
||||
]);
|
||||
|
||||
const dispatchSettings = (updatedSettings: Record<string, string>) => {
|
||||
let i = 0;
|
||||
for (const [key, value] of Object.entries(updatedSettings)) {
|
||||
store.dispatch(setByKey({ key, value }));
|
||||
if (key in DISPLAY_MAP) {
|
||||
setTimeout(() => {
|
||||
toast.settingsChanged(`Set ${DISPLAY_MAP[key]} to "${value}"`);
|
||||
}, i * 500);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeAgent = () => {
|
||||
const event = { action: ActionType.INIT, args: getCurrentSettings() };
|
||||
// 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,
|
||||
]),
|
||||
);
|
||||
const event = { action: "initialize", args: socketSettings };
|
||||
const eventString = JSON.stringify(event);
|
||||
socket.send(eventString);
|
||||
store.dispatch(setInitialized(false));
|
||||
Socket.send(eventString);
|
||||
};
|
||||
|
||||
// Save and send settings to the server
|
||||
export function saveSettings(newSettings: Settings): void {
|
||||
const currentSettings = getCurrentSettings();
|
||||
const updatedSettings = getUpdatedSettings(newSettings, currentSettings);
|
||||
|
||||
if (Object.keys(updatedSettings).length === 0) {
|
||||
return;
|
||||
if (appendMessages) {
|
||||
for (const [setting, value] of Object.entries(reduxSettings)) {
|
||||
store.dispatch(appendAssistantMessage(`Set ${setting} to "${value}"`));
|
||||
}
|
||||
}
|
||||
|
||||
dispatchSettings(updatedSettings);
|
||||
initializeAgent();
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
// 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: [],
|
||||
};
|
||||
|
||||
private static initializing = false;
|
||||
|
||||
public static tryInitialize(): void {
|
||||
if (Socket.initializing) return;
|
||||
Socket.initializing = true;
|
||||
getToken()
|
||||
.then((token) => {
|
||||
Socket._initialize(token);
|
||||
})
|
||||
.catch(() => {
|
||||
const msg = `Connection failed. Retry...`;
|
||||
toast.stickyError("ws", msg);
|
||||
|
||||
setTimeout(() => {
|
||||
this.tryInitialize();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
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.initializing = false;
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
static isConnected(): boolean {
|
||||
return (
|
||||
Socket._socket !== null && Socket._socket.readyState === WebSocket.OPEN
|
||||
);
|
||||
}
|
||||
|
||||
static send(message: string): void {
|
||||
if (!Socket.isConnected()) {
|
||||
Socket.tryInitialize();
|
||||
}
|
||||
if (Socket.initializing) {
|
||||
setTimeout(() => Socket.send(message), 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user