mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
15 Commits
0.25.0
...
fix/git-lf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
164fab0a8d | ||
|
|
bddf6674c3 | ||
|
|
0180ce77b1 | ||
|
|
2f14e53746 | ||
|
|
52723061b1 | ||
|
|
42f1fc92fa | ||
|
|
3f8bc8a7ea | ||
|
|
f869ad995c | ||
|
|
74c942c911 | ||
|
|
eed7e2dd6e | ||
|
|
663e36109c | ||
|
|
e92e4a1cbc | ||
|
|
61ce673400 | ||
|
|
b95840db0c | ||
|
|
003ebc0ded |
16
.github/workflows/openhands-resolver.yml
vendored
16
.github/workflows/openhands-resolver.yml
vendored
@@ -88,12 +88,10 @@ jobs:
|
||||
run: |
|
||||
python -m pip index versions openhands-ai > openhands_versions.txt
|
||||
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
|
||||
# Ensure requirements.txt ends with newline before appending
|
||||
if [ -f requirements.txt ] && [ -s requirements.txt ]; then
|
||||
sed -i -e '$a\' requirements.txt
|
||||
fi
|
||||
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
|
||||
cat requirements.txt
|
||||
|
||||
# Create a new requirements.txt locally within the workflow, ensuring no reference to the repo's file
|
||||
echo "openhands-ai==${OPENHANDS_VERSION}" > /tmp/requirements.txt
|
||||
cat /tmp/requirements.txt
|
||||
|
||||
- name: Cache pip dependencies
|
||||
if: |
|
||||
@@ -111,9 +109,9 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
|
||||
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
|
||||
|
||||
- name: Check required environment variables
|
||||
env:
|
||||
@@ -225,7 +223,7 @@ jobs:
|
||||
} else {
|
||||
console.log("Installing from requirements.txt...");
|
||||
await exec.exec("python -m pip install --upgrade pip");
|
||||
await exec.exec("pip install -r requirements.txt");
|
||||
await exec.exec("pip install -r /tmp/requirements.txt");
|
||||
}
|
||||
|
||||
- name: Attempt to resolve issue
|
||||
|
||||
@@ -46,3 +46,11 @@ docker run -it \
|
||||
-e THAT=that
|
||||
...
|
||||
```
|
||||
|
||||
### Referring to UI Elements
|
||||
|
||||
When referencing UI elements, use ``.
|
||||
|
||||
Example:
|
||||
1. Toggle the `Advanced` option
|
||||
2. Enter your model in the `Custom Model` textbox.
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# GUI Mode
|
||||
|
||||
## Introduction
|
||||
|
||||
OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant.
|
||||
This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI.
|
||||
OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant.
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
@@ -14,104 +11,95 @@ This mode offers an intuitive way to set up the environment, manage settings, an
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Upon first launch, you'll see a settings modal.
|
||||
2. Select an `LLM Provider` and `LLM Model` from the dropdown menus.
|
||||
1. Upon first launch, you'll see a settings page.
|
||||
2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. If the required model does not exist in the list,
|
||||
toggle `Advanced` options and enter it with the correct prefix in the `Custom Model` text box.
|
||||
3. Enter the corresponding `API Key` for your chosen provider.
|
||||
4. Click "Save" to apply the settings.
|
||||
4. Click `Save Changes` to apply the settings.
|
||||
|
||||
### GitHub Token Setup
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
|
||||
|
||||
- **Locally (OSS)**: The user directly inputs their GitHub token.
|
||||
- **Online (SaaS)**: The token is obtained through GitHub OAuth authentication.
|
||||
|
||||
#### Setting Up a Local GitHub Token
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
- Click "Generate new token (classic)".
|
||||
- **Local Installation**: The user directly inputs their GitHub token.
|
||||
<details>
|
||||
<summary>Setting Up a GitHub Token</summary>
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
- Click `Generate new token (classic)`.
|
||||
- Required scopes:
|
||||
- `repo` (Full control of private repositories)
|
||||
- `workflow` (Update GitHub Action workflows)
|
||||
- `read:org` (Read organization data)
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `GitHub Settings` section.
|
||||
- Paste your token in the `GitHub Token` field.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
</details>
|
||||
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon) in the top right.
|
||||
- Navigate to the "GitHub" section.
|
||||
- Paste your token in the "GitHub Token" field.
|
||||
- Click "Save" to apply the changes.
|
||||
<details>
|
||||
<summary>Organizational Token Policies</summary>
|
||||
|
||||
#### Organizational Token Policies
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
1. **Check Organization Requirements**:
|
||||
1. **Check Organization Requirements**:
|
||||
- Organization admins may enforce specific token policies.
|
||||
- Some organizations require tokens to be created with SSO enabled.
|
||||
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization).
|
||||
|
||||
2. **Verify Organization Access**:
|
||||
2. **Verify Organization Access**:
|
||||
- Go to your token settings on GitHub.
|
||||
- Look for the organization under "Organization access".
|
||||
- If required, click "Enable SSO" next to your organization.
|
||||
- Look for the organization under `Organization access`.
|
||||
- If required, click `Enable SSO` next to your organization.
|
||||
- Complete the SSO authorization process.
|
||||
</details>
|
||||
|
||||
#### OAuth Authentication (Online Mode)
|
||||
<details>
|
||||
<summary>Troubleshooting</summary>
|
||||
|
||||
When using OpenHands in online mode, the GitHub OAuth flow:
|
||||
Common issues and solutions:
|
||||
|
||||
1. Requests the following permissions:
|
||||
- **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- Try regenerating the token.
|
||||
|
||||
- **Organization Access Denied**:
|
||||
- Check if SSO is required but not enabled.
|
||||
- Verify organization membership.
|
||||
- Contact organization admin if token policies are blocking access.
|
||||
|
||||
- **Verifying Token Works**:
|
||||
- The app will show a green checkmark if the token is valid.
|
||||
- Try accessing a repository to confirm permissions.
|
||||
- Check the browser console for any error messages.
|
||||
</details>
|
||||
|
||||
- **OpenHands Cloud**: The token is obtained through GitHub OAuth authentication.
|
||||
|
||||
<details>
|
||||
<summary>OAuth Authentication</summary>
|
||||
|
||||
When using OpenHands Cloud, the GitHub OAuth flow requests the following permissions:
|
||||
- Repository access (read/write)
|
||||
- Workflow management
|
||||
- Organization read access
|
||||
|
||||
2. Authentication steps:
|
||||
- Click "Sign in with GitHub" when prompted.
|
||||
To authenticate OpenHands:
|
||||
- Click `Sign in with GitHub` when prompted.
|
||||
- Review the requested permissions.
|
||||
- Authorize OpenHands to access your GitHub account.
|
||||
- If using an organization, authorize organization access if prompted.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
- **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- Try regenerating the token.
|
||||
|
||||
- **Organization Access Denied**:
|
||||
- Check if SSO is required but not enabled.
|
||||
- Verify organization membership.
|
||||
- Contact organization admin if token policies are blocking access.
|
||||
|
||||
- **Verifying Token Works**:
|
||||
- The app will show a green checkmark if the token is valid.
|
||||
- Try accessing a repository to confirm permissions.
|
||||
- Check the browser console for any error messages.
|
||||
- Use the "Test Connection" button in settings if available.
|
||||
</details>
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
1. Toggle `Advanced Options` to access additional settings.
|
||||
1. Inside the Settings page, toggle `Advanced` options to access additional settings.
|
||||
2. Use the `Custom Model` text box to manually enter a model if it's not in the list.
|
||||
3. Specify a `Base URL` if required by your LLM provider.
|
||||
|
||||
### Main Interface
|
||||
|
||||
The main interface consists of several key components:
|
||||
|
||||
- **Chat Window**: The central area where you can view the conversation history with the AI assistant.
|
||||
- **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI.
|
||||
- **Send Button**: Click this to send your message to the AI.
|
||||
- **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time.
|
||||
- **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history.
|
||||
|
||||
### Interacting with the AI
|
||||
|
||||
1. Type your question, request, or task description in the input box.
|
||||
1. Type your prompt in the input box.
|
||||
2. Click the send button or press Enter to submit your message.
|
||||
3. The AI will process your input and provide a response in the chat window.
|
||||
4. You can continue the conversation by asking follow-up questions or providing additional information.
|
||||
|
||||
@@ -12,7 +12,8 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
|
||||
<details>
|
||||
<summary>MacOS</summary>
|
||||
### Docker Desktop
|
||||
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install).
|
||||
2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled.
|
||||
@@ -25,7 +26,7 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
Tested with Ubuntu 22.04.
|
||||
:::
|
||||
|
||||
### Docker Desktop
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/).
|
||||
|
||||
@@ -33,12 +34,13 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
|
||||
<details>
|
||||
<summary>Windows</summary>
|
||||
### WSL
|
||||
|
||||
**WSL**
|
||||
|
||||
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
|
||||
|
||||
### Docker Desktop
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
|
||||
2. Open Docker Desktop, go to `Settings` and confirm the following:
|
||||
@@ -78,24 +80,22 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
|
||||
|
||||
## Setup
|
||||
|
||||
Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
|
||||
Upon launching OpenHands, you'll see a Settings page. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
|
||||
These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
|
||||
|
||||
If the required `LLM Model` does not exist in the list, you can toggle `Advanced Options` and manually enter it with the correct prefix
|
||||
If the required model does not exist in the list, you can toggle `Advanced` options and manually enter it with the correct prefix
|
||||
in the `Custom Model` text box.
|
||||
The `Advanced Options` also allow you to specify a `Base URL` if required.
|
||||
The `Advanced` options also allow you to specify a `Base URL` if required.
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '20px' }}>
|
||||
<img src="/img/settings-screenshot.png" alt="settings-modal" width="340" />
|
||||
<img src="/img/settings-advanced.png" alt="settings-modal" width="335" />
|
||||
</div>
|
||||
Now you're ready to [get started with OpenHands](./getting-started).
|
||||
|
||||
## Versions
|
||||
|
||||
The command above pulls the most recent stable release of OpenHands. You have other options as well:
|
||||
- For a specific release, use `docker.all-hands.dev/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number.
|
||||
- We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
|
||||
- For the most up-to-date development version, you can use `docker.all-hands.dev/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only.
|
||||
The [docker command above](./installation#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well:
|
||||
- For a specific release, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with the version number.
|
||||
We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
|
||||
- For the most up-to-date development version, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with `main`.
|
||||
This version is unstable and is recommended for testing or development purposes only.
|
||||
|
||||
You can choose the tag that best suits your needs based on stability requirements and desired features.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ You will need your ChatGPT deployment name which can be found on the deployments
|
||||
<deployment-name> below.
|
||||
:::
|
||||
|
||||
1. Enable `Advanced Options`
|
||||
1. Enable `Advanced` options
|
||||
2. Set the following:
|
||||
- `Custom Model` to azure/<deployment-name>
|
||||
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
|
||||
|
||||
@@ -10,7 +10,7 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `Gemini`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`).
|
||||
- `API Key` to your Gemini API key
|
||||
|
||||
## VertexAI - Google Cloud Platform Configs
|
||||
@@ -27,4 +27,4 @@ VERTEXAI_LOCATION="<your-gcp-location>"
|
||||
Then set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `VertexAI`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>).
|
||||
|
||||
@@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
- `LLM Provider` to `Groq`
|
||||
- `LLM Model` to the model you will be using. [Visit here to see the list of
|
||||
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list, toggle
|
||||
`Advanced Options`, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`).
|
||||
`Advanced` options, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`).
|
||||
- `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ models that Groq hosts](https://console.groq.com/docs/models). If the model is n
|
||||
|
||||
The Groq endpoint for chat completion is [mostly OpenAI-compatible](https://console.groq.com/docs/openai). Therefore, you can access Groq models as you
|
||||
would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings:
|
||||
1. Enable `Advanced Options`
|
||||
1. Enable `Advanced` options
|
||||
2. Set the following:
|
||||
- `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`)
|
||||
- `Base URL` to `https://api.groq.com/openai/v1`
|
||||
|
||||
@@ -8,7 +8,7 @@ To use LiteLLM proxy with OpenHands, you need to:
|
||||
|
||||
1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start))
|
||||
2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
* Enable `Advanced Options`
|
||||
* Enable `Advanced` options
|
||||
* `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`)
|
||||
* `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`)
|
||||
* `API Key` to your LiteLLM proxy API key
|
||||
|
||||
@@ -38,7 +38,7 @@ The following can be set in the OpenHands UI through the Settings:
|
||||
- `LLM Provider`
|
||||
- `LLM Model`
|
||||
- `API Key`
|
||||
- `Base URL` (through `Advanced Settings`)
|
||||
- `Base URL` (through `Advanced` settings)
|
||||
|
||||
There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these
|
||||
can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app)
|
||||
|
||||
@@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
* `LLM Provider` to `OpenAI`
|
||||
* `LLM Model` to the model you will be using.
|
||||
[Visit here to see a full list of OpenAI models that LiteLLM supports.](https://docs.litellm.ai/docs/providers/openai#openai-chat-completion-models)
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`).
|
||||
* `API Key` to your OpenAI API key. To find or create your OpenAI Project API Key, [see here](https://platform.openai.com/api-keys).
|
||||
|
||||
## Using OpenAI-Compatible Endpoints
|
||||
@@ -18,7 +18,7 @@ Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoi
|
||||
## Using an OpenAI Proxy
|
||||
|
||||
If you're using an OpenAI proxy, in the OpenHands UI through the Settings:
|
||||
1. Enable `Advanced Options`
|
||||
1. Enable `Advanced` options
|
||||
2. Set the following:
|
||||
- `Custom Model` to openai/<model-name> (e.g. `openai/gpt-4o` or openai/<proxy-prefix>/<model-name>)
|
||||
- `Base URL` to the URL of your OpenAI proxy
|
||||
|
||||
@@ -8,5 +8,5 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
* `LLM Provider` to `OpenRouter`
|
||||
* `LLM Model` to the model you will be using.
|
||||
[Visit here to see a full list of OpenRouter models](https://openrouter.ai/models).
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`).
|
||||
* `API Key` to your OpenRouter API key.
|
||||
|
||||
@@ -66,7 +66,7 @@ const sidebars: SidebarsConfig = {
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Github Actions',
|
||||
label: 'Github Action',
|
||||
id: 'usage/how-to/github-action',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,6 +23,17 @@ export default function Home(): JSX.Element {
|
||||
})}
|
||||
>
|
||||
<HomepageHeader />
|
||||
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<br />
|
||||
<h2>Most Popular Links</h2>
|
||||
<ul style={{ listStyleType: 'none'}}>
|
||||
<li><a href="/modules/usage/Installation">How to Run OpenHands</a></li>
|
||||
<li><a href="/modules/usage/prompting/microagents-repo">Customizing OpenHands to a repository</a></li>
|
||||
<li><a href="/modules/usage/how-to/github-action">Integrating OpenHands with Github</a></li>
|
||||
<li><a href="/modules/usage/llms#model-recommendations">Recommended models to use</a></li>
|
||||
<li><a href="/modules/usage/runtimes#connecting-to-your-filesystem">Connecting OpenHands to your filesystem</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
docs/static/img/settings-advanced.png
vendored
BIN
docs/static/img/settings-advanced.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/settings-screenshot.png
vendored
BIN
docs/static/img/settings-screenshot.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
@@ -78,12 +78,6 @@ describe("PaymentForm", () => {
|
||||
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
|
||||
});
|
||||
|
||||
it("should render the payment method link", async () => {
|
||||
renderPaymentForm();
|
||||
|
||||
screen.getByTestId("payment-methods-link");
|
||||
});
|
||||
|
||||
it("should disable the top-up button if the user enters an invalid amount", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
@@ -155,7 +149,7 @@ describe("PaymentForm", () => {
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "20"); // test assumes the minimum is 25
|
||||
await user.type(topUpInput, "9"); // test assumes the minimum is 10
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
@@ -24,9 +24,15 @@ describe("amountIsValid", () => {
|
||||
});
|
||||
|
||||
test("when an amount less than the minimum is passed", () => {
|
||||
// test assumes the minimum is 25
|
||||
expect(amountIsValid("24")).toBe(false);
|
||||
expect(amountIsValid("24.99")).toBe(false);
|
||||
// test assumes the minimum is 10
|
||||
expect(amountIsValid("9")).toBe(false);
|
||||
expect(amountIsValid("9.99")).toBe(false);
|
||||
});
|
||||
|
||||
test("when an amount more than the maximum is passed", () => {
|
||||
// test assumes the minimum is 25000
|
||||
expect(amountIsValid("25001")).toBe(false);
|
||||
expect(amountIsValid("25000.01")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ export function AgentStatusBar() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-center bg-neutral-800 px-2 py-1 text-gray-400 rounded-[100px] text-sm gap-[6px]">
|
||||
<div className="flex items-center bg-base-secondary px-2 py-1 text-gray-400 rounded-[100px] text-sm gap-[6px]">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full animate-pulse ${AGENT_STATUS_MAP[curAgentState].indicator}`}
|
||||
/>
|
||||
|
||||
@@ -73,7 +73,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
<div
|
||||
ref={ref}
|
||||
data-testid="conversation-panel"
|
||||
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl overflow-y-auto"
|
||||
className="w-[350px] h-full border border-neutral-700 bg-base-secondary rounded-xl overflow-y-auto"
|
||||
>
|
||||
<div className="pt-4 px-4 flex items-center justify-between">
|
||||
{isFetching && <LoadingSpinner size="small" />}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function FileExplorerHeader({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-0 bg-neutral-800",
|
||||
"sticky top-0 bg-base-secondary",
|
||||
"flex items-center",
|
||||
!isOpen ? "justify-center" : "justify-between",
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
<div data-testid="file-explorer" className="relative h-full">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-neutral-800 h-full border-r-1 border-r-neutral-600 flex flex-col",
|
||||
"bg-base-secondary h-full border-r-1 border-r-neutral-600 flex flex-col",
|
||||
!isOpen ? "w-12" : "w-60",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { cn } from "#/utils/utils";
|
||||
import MoneyIcon from "#/icons/money.svg?react";
|
||||
import { SettingsInput } from "../settings/settings-input";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { HelpLink } from "../settings/help-link";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { amountIsValid } from "#/utils/amount-is-valid";
|
||||
|
||||
@@ -80,13 +79,6 @@ export function PaymentForm() {
|
||||
{isPending && <LoadingSpinner size="small" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HelpLink
|
||||
testId="payment-methods-link"
|
||||
href="https://stripe.com/"
|
||||
text="Manage payment methods on"
|
||||
linkText="Stripe"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ export function BrandButton({
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-fit p-2 rounded disabled:opacity-30 disabled:cursor-not-allowed",
|
||||
variant === "primary" && "bg-[#C9B974] text-[#0D0F11]",
|
||||
variant === "secondary" && "border border-[#C9B974] text-[#C9B974]",
|
||||
variant === "primary" && "bg-primary text-[#0D0F11]",
|
||||
variant === "secondary" && "border border-primary text-primary",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,9 +8,7 @@ interface KeyStatusIconProps {
|
||||
export function KeyStatusIcon({ isSet }: KeyStatusIconProps) {
|
||||
return (
|
||||
<span data-testid={isSet ? "set-indicator" : "unset-indicator"}>
|
||||
<SuccessIcon
|
||||
className={cn(isSet ? "text-[#A5E75E]" : "text-[#E76A5E]")}
|
||||
/>
|
||||
<SuccessIcon className={cn(isSet ? "text-success" : "text-danger")} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function OptionalTag() {
|
||||
return <span className="text-xs text-[#B7BDC2]">(Optional)</span>;
|
||||
return <span className="text-xs text-tertiary-alt">(Optional)</span>;
|
||||
}
|
||||
|
||||
@@ -38,12 +38,12 @@ export function SettingsDropdownInput({
|
||||
isDisabled={isDisabled}
|
||||
className="w-full"
|
||||
classNames={{
|
||||
popoverContent: "bg-[#454545] rounded-xl border border-[#717888]",
|
||||
popoverContent: "bg-tertiary rounded-xl border border-[#717888]",
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SettingsInput({
|
||||
defaultValue={defaultValue}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
"bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-[#B7BDC2]",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SettingsSwitch({
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm">{children}</span>
|
||||
{isBeta && (
|
||||
<span className="text-[11px] leading-4 text-[#0D0F11] font-[500] tracking-tighter bg-[#C9B974] px-1 rounded-full">
|
||||
<span className="text-[11px] leading-4 text-[#0D0F11] font-[500] tracking-tighter bg-primary px-1 rounded-full">
|
||||
Beta
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -11,14 +11,14 @@ export function StyledSwitchComponent({
|
||||
<div
|
||||
className={cn(
|
||||
"w-12 h-6 rounded-xl flex items-center p-1.5 cursor-pointer",
|
||||
isToggled && "justify-end bg-[#C9B974]",
|
||||
!isToggled && "justify-start bg-[#1F2228] border border-[#B7BDC2]",
|
||||
isToggled && "justify-end bg-primary",
|
||||
!isToggled && "justify-start bg-[#1F2228] border border-tertiary-alt",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-[#1F2228] w-3 h-3 rounded-xl",
|
||||
isToggled ? "bg-[#1F2228]" : "bg-[#B7BDC2]",
|
||||
isToggled ? "bg-[#1F2228]" : "bg-tertiary-alt",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
export function BetaBadge() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl">
|
||||
<span className="text-[11px] leading-5 text-base bg-neutral-400 px-1 rounded-xl">
|
||||
{t(I18nKey.BADGE$BETA)}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Container({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-neutral-800 border border-neutral-600 rounded-xl flex flex-col",
|
||||
"bg-base-secondary border border-neutral-600 rounded-xl flex flex-col",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function CountBadge({ count }: { count: number }) {
|
||||
return (
|
||||
<span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl">
|
||||
<span className="text-[11px] leading-5 text-base bg-neutral-400 px-1 rounded-xl">
|
||||
{count}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,10 @@ export function NavTab({ to, label, icon, isBeta }: NavTabProps) {
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"px-2 border-b border-r border-neutral-600 bg-root-primary flex-1",
|
||||
"px-2 border-b border-r border-neutral-600 bg-base flex-1",
|
||||
"first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0",
|
||||
"flex items-center gap-2",
|
||||
isActive && "bg-root-secondary",
|
||||
isActive && "bg-base-secondary",
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ActionTooltip({ type, onClick }: ActionTooltipProps) {
|
||||
? t(I18nKey.ACTION$CONFIRM)
|
||||
: t(I18nKey.ACTION$REJECT)
|
||||
}
|
||||
className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
|
||||
className="bg-neutral-700 rounded-full p-1 hover:bg-base-secondary"
|
||||
onClick={onClick}
|
||||
>
|
||||
{type === "confirm" ? <ConfirmIcon /> : <RejectIcon />}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
|
||||
ariaLabel="All Hands Logo"
|
||||
onClick={onClick}
|
||||
>
|
||||
<AllHandsLogo width={34} height={23} />
|
||||
<AllHandsLogo width={34} height={34} />
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function HeroHeading() {
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://docs.all-hands.dev/modules/usage/getting-started"
|
||||
className="text-hyperlink underline underline-offset-[3px]"
|
||||
className="text-white underline underline-offset-[3px]"
|
||||
>
|
||||
{t(I18nKey.LANDING$START_HELP_LINK)}
|
||||
</a>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function BaseModal({
|
||||
backdrop="blur"
|
||||
hideCloseButton
|
||||
size="sm"
|
||||
className="bg-neutral-900 rounded-lg"
|
||||
className="bg-base rounded-lg"
|
||||
>
|
||||
<ModalContent className={contentClassName}>
|
||||
{(closeModal) => (
|
||||
|
||||
@@ -12,7 +12,7 @@ export function ModalBody({ testID, children, className }: ModalBodyProps) {
|
||||
<div
|
||||
data-testid={testID}
|
||||
className={cn(
|
||||
"bg-root-primary flex flex-col gap-6 items-center w-[384px] p-6 rounded-xl",
|
||||
"bg-base flex flex-col gap-6 items-center w-[384px] p-6 rounded-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -247,7 +247,7 @@ function SecurityInvariant() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 w-full h-full">
|
||||
<div className="w-60 bg-neutral-800 border-r border-r-neutral-600 p-4 flex-shrink-0">
|
||||
<div className="w-60 bg-base-secondary border-r border-r-neutral-600 p-4 flex-shrink-0">
|
||||
<div className="text-center mb-2">
|
||||
<InvariantLogoIcon className="mx-auto mb-1" />
|
||||
<b>{t(I18nKey.INVARIANT$INVARIANT_ANALYZER_LABEL)}</b>
|
||||
@@ -285,7 +285,7 @@ function SecurityInvariant() {
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col min-h-0 w-full overflow-y-auto bg-neutral-900">
|
||||
<div className="flex flex-col min-h-0 w-full overflow-y-auto bg-base">
|
||||
{sections[activeSection as SectionType]}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,12 +84,12 @@ export function ModelSelector({
|
||||
defaultSelectedKey={selectedProvider ?? undefined}
|
||||
selectedKey={selectedProvider}
|
||||
classNames={{
|
||||
popoverContent: "bg-[#454545] rounded-xl border border-[#717888]",
|
||||
popoverContent: "bg-tertiary rounded-xl border border-[#717888]",
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -135,12 +135,12 @@ export function ModelSelector({
|
||||
selectedKey={selectedModel}
|
||||
defaultSelectedKey={selectedModel ?? undefined}
|
||||
classNames={{
|
||||
popoverContent: "bg-[#454545] rounded-xl border border-[#717888]",
|
||||
popoverContent: "bg-tertiary rounded-xl border border-[#717888]",
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
inputWrapper:
|
||||
"bg-[#454545] border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
|
||||
<ModalBackdrop>
|
||||
<div
|
||||
data-testid="ai-config-modal"
|
||||
className="bg-root-primary min-w-[384px] p-6 rounded-xl flex flex-col gap-2"
|
||||
className="bg-base min-w-[384px] p-6 rounded-xl flex flex-col gap-2"
|
||||
>
|
||||
{aiConfigOptions.error && (
|
||||
<p className="text-danger text-xs">{aiConfigOptions.error.message}</p>
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
--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;
|
||||
--text-editor-base: #9099ac;
|
||||
--text-editor-active: #c4cbda;
|
||||
--bg-editor-sidebar: #24272e;
|
||||
--bg-editor-active: #31343d;
|
||||
--border-editor-sidebar: #3c3c4a;
|
||||
background-color: var(--base) !important;
|
||||
--bg-neutral-muted: #afb8c133;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
font-family:
|
||||
-apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -23,8 +24,8 @@ body {
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
@@ -52,6 +53,7 @@ code {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body th, .markdown-body td {
|
||||
.markdown-body th,
|
||||
.markdown-body td {
|
||||
padding: 0.1rem 1rem;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ function Home() {
|
||||
const latestConversation = localStorage.getItem("latest_conversation_id");
|
||||
|
||||
return (
|
||||
<div className="bg-root-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2">
|
||||
<div className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2">
|
||||
<HeroHeading />
|
||||
<div className="flex flex-col gap-8 w-full md:w-[600px] items-center">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
|
||||
@@ -75,7 +75,7 @@ function FileViewer() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full bg-neutral-900 relative">
|
||||
<div className="flex h-full bg-base relative">
|
||||
<FileExplorer isOpen={fileExplorerIsOpen} onToggle={toggleFileExplorer} />
|
||||
<div className="w-full h-full flex flex-col">
|
||||
{selectedPath && (
|
||||
|
||||
@@ -127,7 +127,7 @@ function AppContent() {
|
||||
orientation={Orientation.HORIZONTAL}
|
||||
className="grow h-full min-h-0 min-w-0"
|
||||
initialSize={500}
|
||||
firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-neutral-800"
|
||||
firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-base-secondary"
|
||||
secondClassName="flex flex-col overflow-hidden"
|
||||
firstChild={<ChatInterface />}
|
||||
secondChild={
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function MainApp() {
|
||||
return (
|
||||
<div
|
||||
data-testid="root-layout"
|
||||
className="bg-root-primary p-3 h-screen md:min-w-[1024px] overflow-x-hidden flex flex-col md:flex-row gap-3"
|
||||
className="bg-base p-3 h-screen md:min-w-[1024px] overflow-x-hidden flex flex-col md:flex-row gap-3"
|
||||
>
|
||||
<Sidebar />
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ function AccountSettings() {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<footer className="flex gap-6 p-6 justify-end border-t border-t-[#454545]">
|
||||
<footer className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -411,7 +411,7 @@ function AccountSettings() {
|
||||
<ModalBackdrop>
|
||||
<div
|
||||
data-testid="reset-modal"
|
||||
className="bg-root-primary p-4 rounded-xl flex flex-col gap-4"
|
||||
className="bg-base p-4 rounded-xl flex flex-col gap-4"
|
||||
>
|
||||
<p>Are you sure you want to reset all settings?</p>
|
||||
<div className="w-full flex gap-2">
|
||||
|
||||
@@ -11,9 +11,9 @@ function SettingsScreen() {
|
||||
return (
|
||||
<main
|
||||
data-testid="settings-screen"
|
||||
className="bg-[#24272E] border border-[#454545] h-full rounded-xl flex flex-col"
|
||||
className="bg-base-secondary border border-tertiary h-full rounded-xl flex flex-col"
|
||||
>
|
||||
<header className="px-3 py-1.5 border-b border-b-[#454545] flex items-center gap-2">
|
||||
<header className="px-3 py-1.5 border-b border-b-tertiary flex items-center gap-2">
|
||||
<SettingsIcon width={16} height={16} />
|
||||
<h1 className="text-sm leading-6">Settings</h1>
|
||||
</header>
|
||||
@@ -21,7 +21,7 @@ function SettingsScreen() {
|
||||
{isSaas && BILLING_SETTINGS() && (
|
||||
<nav
|
||||
data-testid="settings-navbar"
|
||||
className="flex items-end gap-12 px-11 border-b border-[#454545]"
|
||||
className="flex items-end gap-12 px-11 border-b border-tertiary"
|
||||
>
|
||||
{[
|
||||
{ to: "/settings", text: "Account" },
|
||||
@@ -34,7 +34,7 @@ function SettingsScreen() {
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"border-b-2 border-transparent py-2.5",
|
||||
isActive && "border-[#C9B974]",
|
||||
isActive && "border-primary",
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const MINIMUM_AMOUNT = 25;
|
||||
const MINIMUM_AMOUNT = 10;
|
||||
const MAXIMUM_AMOUNT = 25_000;
|
||||
|
||||
export const amountIsValid = (amount: string) => {
|
||||
const float = parseFloat(amount);
|
||||
if (Number.isNaN(float)) return false;
|
||||
if (float < 0) return false;
|
||||
if (float < MINIMUM_AMOUNT) return false;
|
||||
if (float > MAXIMUM_AMOUNT) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import { heroui } from "@heroui/react";
|
||||
import typography from '@tailwindcss/typography';
|
||||
import typography from "@tailwindcss/typography";
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
@@ -9,11 +9,13 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'root-primary': '#171717',
|
||||
'root-secondary': '#262626',
|
||||
'hyperlink': '#007AFF',
|
||||
'danger': '#EF3744',
|
||||
'success': '#4CAF50',
|
||||
primary: "#C9B974", // nice yellow
|
||||
base: "#171717", // dark background (neutral-900)
|
||||
"base-secondary": "#262626", // lighter background (neutral-800); also used for tooltips
|
||||
danger: "#E76A5E",
|
||||
success: "#A5E75E",
|
||||
tertiary: "#454545", // gray, used for inputs
|
||||
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -32,8 +34,8 @@ export default {
|
||||
colors: {
|
||||
primary: "#4465DB",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
typography,
|
||||
],
|
||||
|
||||
@@ -22,7 +22,6 @@ from openhands.events.observation import (
|
||||
CmdOutputObservation,
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
@@ -109,7 +108,7 @@ class DummyAgent(Agent):
|
||||
},
|
||||
{
|
||||
'action': AgentRejectAction(),
|
||||
'observations': [NullObservation('')],
|
||||
'observations': [AgentStateChangedObservation('', AgentState.REJECTED)],
|
||||
},
|
||||
{
|
||||
'action': AgentFinishAction(
|
||||
|
||||
@@ -6,11 +6,11 @@ from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import Action
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.serialization.event import event_to_memory
|
||||
from openhands.io import json
|
||||
from openhands.llm.llm import LLM
|
||||
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ from openhands.events.observation import (
|
||||
AgentStateChangedObservation,
|
||||
CmdOutputObservation,
|
||||
FileEditObservation,
|
||||
NullObservation,
|
||||
)
|
||||
from openhands.io import read_input, read_task
|
||||
|
||||
|
||||
def display_message(message: str):
|
||||
@@ -83,21 +83,6 @@ def display_event(event: Event, config: AppConfig):
|
||||
display_confirmation(event.confirmation_state)
|
||||
|
||||
|
||||
def read_input(config: AppConfig) -> str:
|
||||
"""Read input from user based on config settings."""
|
||||
if config.cli_multiline_input:
|
||||
print('Enter your message (enter "/exit" on a new line to finish):')
|
||||
lines = []
|
||||
while True:
|
||||
line = input('>> ').rstrip()
|
||||
if line == '/exit': # finish input
|
||||
break
|
||||
lines.append(line)
|
||||
return '\n'.join(lines)
|
||||
else:
|
||||
return input('>> ').rstrip()
|
||||
|
||||
|
||||
async def main(loop: asyncio.AbstractEventLoop):
|
||||
"""Runs the agent in CLI mode."""
|
||||
|
||||
@@ -105,7 +90,14 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
config = setup_config_from_args(args)
|
||||
# Load config from toml and override with command line arguments
|
||||
config: AppConfig = setup_config_from_args(args)
|
||||
|
||||
# Read task from file, CLI args, or stdin
|
||||
task_str = read_task(args, config.cli_multiline_input)
|
||||
|
||||
# If we have a task, create initial user action
|
||||
initial_user_action = MessageAction(content=task_str) if task_str else None
|
||||
|
||||
sid = str(uuid4())
|
||||
|
||||
@@ -118,7 +110,9 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
|
||||
async def prompt_for_next_task():
|
||||
# Run input() in a thread pool to avoid blocking the event loop
|
||||
next_message = await loop.run_in_executor(None, read_input, config)
|
||||
next_message = await loop.run_in_executor(
|
||||
None, read_input, config.cli_multiline_input
|
||||
)
|
||||
if not next_message.strip():
|
||||
await prompt_for_next_task()
|
||||
if next_message == 'exit':
|
||||
@@ -143,19 +137,18 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
AgentState.FINISHED,
|
||||
]:
|
||||
await prompt_for_next_task()
|
||||
if (
|
||||
isinstance(event, NullObservation)
|
||||
and controller.state.agent_state == AgentState.AWAITING_USER_CONFIRMATION
|
||||
):
|
||||
user_confirmed = await prompt_for_user_confirmation()
|
||||
if user_confirmed:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_CONFIRMED), EventSource.USER
|
||||
)
|
||||
else:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_REJECTED), EventSource.USER
|
||||
)
|
||||
if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
|
||||
user_confirmed = await prompt_for_user_confirmation()
|
||||
if user_confirmed:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_CONFIRMED),
|
||||
EventSource.USER,
|
||||
)
|
||||
else:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_REJECTED),
|
||||
EventSource.USER,
|
||||
)
|
||||
|
||||
def on_event(event: Event) -> None:
|
||||
loop.create_task(on_event_async(event))
|
||||
@@ -164,7 +157,12 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
|
||||
await runtime.connect()
|
||||
|
||||
asyncio.create_task(prompt_for_next_task())
|
||||
if initial_user_action:
|
||||
# If there's an initial user action, enqueue it and do not prompt again
|
||||
event_stream.add_event(initial_user_action, EventSource.USER)
|
||||
else:
|
||||
# Otherwise prompt for the user's first message right away
|
||||
asyncio.create_task(prompt_for_next_task())
|
||||
|
||||
await run_agent_until_done(
|
||||
controller, runtime, [AgentState.STOPPED, AgentState.ERROR]
|
||||
|
||||
@@ -76,6 +76,7 @@ class AppConfig(BaseModel):
|
||||
file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*'])
|
||||
runloop_api_key: SecretStr | None = Field(default=None)
|
||||
cli_multiline_input: bool = Field(default=False)
|
||||
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
|
||||
|
||||
@@ -102,3 +102,9 @@ class LLMConfig(BaseModel):
|
||||
os.environ['OR_SITE_URL'] = self.openrouter_site_url
|
||||
if self.openrouter_app_name:
|
||||
os.environ['OR_APP_NAME'] = self.openrouter_app_name
|
||||
|
||||
# Assign an API version for Azure models
|
||||
# While it doesn't seem required, the format supported by the API without version seems old and will likely break.
|
||||
# Azure issue: https://github.com/All-Hands-AI/OpenHands/issues/6777
|
||||
if self.model.startswith('azure') and self.api_version is None:
|
||||
self.api_version = '2024-08-01-preview'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Callable, Protocol
|
||||
|
||||
@@ -29,6 +28,7 @@ from openhands.events.event import Event
|
||||
from openhands.events.observation import AgentStateChangedObservation
|
||||
from openhands.events.serialization import event_from_dict
|
||||
from openhands.events.serialization.event import event_to_trajectory
|
||||
from openhands.io import read_input, read_task
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
@@ -41,32 +41,6 @@ class FakeUserResponseFunc(Protocol):
|
||||
) -> str: ...
|
||||
|
||||
|
||||
def read_task_from_file(file_path: str) -> str:
|
||||
"""Read task from the specified file."""
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def read_task_from_stdin() -> str:
|
||||
"""Read task from stdin."""
|
||||
return sys.stdin.read()
|
||||
|
||||
|
||||
def read_input(config: AppConfig) -> str:
|
||||
"""Read input from user based on config settings."""
|
||||
if config.cli_multiline_input:
|
||||
print('Enter your message (enter "/exit" on a new line to finish):')
|
||||
lines = []
|
||||
while True:
|
||||
line = input('>> ').rstrip()
|
||||
if line == '/exit': # finish input
|
||||
break
|
||||
lines.append(line)
|
||||
return '\n'.join(lines)
|
||||
else:
|
||||
return input('>> ').rstrip()
|
||||
|
||||
|
||||
async def run_controller(
|
||||
config: AppConfig,
|
||||
initial_user_action: Action,
|
||||
@@ -139,7 +113,6 @@ async def run_controller(
|
||||
assert isinstance(
|
||||
initial_user_action, Action
|
||||
), f'initial user actions must be an Action, got {type(initial_user_action)}'
|
||||
# Logging
|
||||
logger.debug(
|
||||
f'Agent Controller Initialized: Running agent {agent.name}, model '
|
||||
f'{agent.llm.config.model}, with actions: {initial_user_action}'
|
||||
@@ -167,7 +140,7 @@ async def run_controller(
|
||||
if exit_on_message:
|
||||
message = '/exit'
|
||||
elif fake_user_response_fn is None:
|
||||
message = read_input(config)
|
||||
message = read_input(config.cli_multiline_input)
|
||||
else:
|
||||
message = fake_user_response_fn(controller.get_state())
|
||||
action = MessageAction(content=message)
|
||||
@@ -268,28 +241,23 @@ def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]:
|
||||
if __name__ == '__main__':
|
||||
args = parse_arguments()
|
||||
|
||||
config = setup_config_from_args(args)
|
||||
config: AppConfig = setup_config_from_args(args)
|
||||
|
||||
# Determine the task
|
||||
task_str = ''
|
||||
if args.file:
|
||||
task_str = read_task_from_file(args.file)
|
||||
elif args.task:
|
||||
task_str = args.task
|
||||
elif not sys.stdin.isatty():
|
||||
task_str = read_task_from_stdin()
|
||||
# Read task from file, CLI args, or stdin
|
||||
task_str = read_task(args, config.cli_multiline_input)
|
||||
|
||||
initial_user_action: Action = NullAction()
|
||||
if config.replay_trajectory_path:
|
||||
if task_str:
|
||||
raise ValueError(
|
||||
'User-specified task is not supported under trajectory replay mode'
|
||||
)
|
||||
elif task_str:
|
||||
initial_user_action = MessageAction(content=task_str)
|
||||
else:
|
||||
|
||||
if not task_str:
|
||||
raise ValueError('No task provided. Please specify a task through -t, -f.')
|
||||
|
||||
# Create initial user action
|
||||
initial_user_action: MessageAction = MessageAction(content=task_str)
|
||||
|
||||
# Set session name
|
||||
session_name = args.name
|
||||
sid = generate_sid(config, session_name)
|
||||
|
||||
@@ -8,9 +8,9 @@ from functools import partial
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.serialization.event import event_from_dict, event_to_dict
|
||||
from openhands.io import json
|
||||
from openhands.storage import FileStore
|
||||
from openhands.storage.locations import (
|
||||
get_conversation_dir,
|
||||
|
||||
@@ -10,7 +10,9 @@ from openhands.events.observation import (
|
||||
|
||||
|
||||
def get_pairs_from_events(events: list[Event]) -> list[tuple[Action, Observation]]:
|
||||
"""Return the history as a list of tuples (action, observation)."""
|
||||
"""Return the history as a list of tuples (action, observation).
|
||||
|
||||
This function is a compatibility function for evals reading and visualization working with old histories."""
|
||||
tuples: list[tuple[Action, Observation]] = []
|
||||
action_map: dict[int, Action] = {}
|
||||
observation_map: dict[int, Observation] = {}
|
||||
|
||||
10
openhands/io/__init__.py
Normal file
10
openhands/io/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from openhands.io.io import read_input, read_task, read_task_from_file
|
||||
from openhands.io.json import dumps, loads
|
||||
|
||||
__all__ = [
|
||||
'read_input',
|
||||
'read_task_from_file',
|
||||
'read_task',
|
||||
'dumps',
|
||||
'loads',
|
||||
]
|
||||
40
openhands/io/io.py
Normal file
40
openhands/io/io.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def read_input(cli_multiline_input: bool = False) -> str:
|
||||
"""Read input from user based on config settings."""
|
||||
if cli_multiline_input:
|
||||
print('Enter your message (enter "/exit" on a new line to finish):')
|
||||
lines = []
|
||||
while True:
|
||||
line = input('>> ').rstrip()
|
||||
if line == '/exit': # finish input
|
||||
break
|
||||
lines.append(line)
|
||||
return '\n'.join(lines)
|
||||
else:
|
||||
return input('>> ').rstrip()
|
||||
|
||||
|
||||
def read_task_from_file(file_path: str) -> str:
|
||||
"""Read task from the specified file."""
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def read_task(args: argparse.Namespace, cli_multiline_input: bool) -> str:
|
||||
"""
|
||||
Read the task from the CLI args, file, or stdin.
|
||||
"""
|
||||
|
||||
# Determine the task
|
||||
task_str = ''
|
||||
if args.file:
|
||||
task_str = read_task_from_file(args.file)
|
||||
elif args.task:
|
||||
task_str = args.task
|
||||
elif not sys.stdin.isatty():
|
||||
task_str = read_input(cli_multiline_input)
|
||||
|
||||
return task_str
|
||||
@@ -172,7 +172,7 @@ class LLM(RetryMixin, DebugMixin):
|
||||
)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Wrapper for the litellm completion function. Logs the input and output of the completion function."""
|
||||
from openhands.core.utils import json
|
||||
from openhands.io import json
|
||||
|
||||
messages: list[dict[str, Any]] | dict[str, Any] = []
|
||||
mock_function_calling = not self.is_function_calling_active()
|
||||
@@ -369,7 +369,7 @@ class LLM(RetryMixin, DebugMixin):
|
||||
# noinspection PyBroadException
|
||||
except Exception:
|
||||
pass
|
||||
from openhands.core.utils import json
|
||||
from openhands.io import json
|
||||
|
||||
logger.debug(f'Model info: {json.dumps(self.model_info, indent=2)}')
|
||||
|
||||
|
||||
@@ -57,22 +57,29 @@ class LLMSummarizingCondenser(RollingCondenser):
|
||||
# Construct prompt for summarization
|
||||
prompt = """You are maintaining state history for an LLM-based code agent. Track:
|
||||
|
||||
USER_CONTEXT: (Preserve essential user requirements, problem descriptions, and clarifications in concise form)
|
||||
|
||||
STATE: {File paths, function signatures, data structures}
|
||||
TESTS: {Failing cases, error messages, outputs}
|
||||
CHANGES: {Code edits, variable updates}
|
||||
DEPS: {Dependencies, imports, external calls}
|
||||
INTENT: {Why changes were made, acceptance criteria}
|
||||
|
||||
SKIP: {Git clones, build logs}
|
||||
SUMMARIZE: {File listings}
|
||||
MAX_LENGTH: Keep summaries under 1000 words
|
||||
PRIORITIZE:
|
||||
1. Capture key user requirements and constraints
|
||||
2. Maintain critical problem context
|
||||
3. Keep all sections concise
|
||||
|
||||
SKIP: {Git clones, build logs, file listings}
|
||||
|
||||
Example history format:
|
||||
USER_CONTEXT: Fix FITS card float representation - "0.009125" becomes "0.009124999999999999" causing comment truncation. Use Python's str() when possible while maintaining FITS compliance.
|
||||
|
||||
STATE: mod_float() in card.py updated
|
||||
TESTS: test_format() passed
|
||||
CHANGES: str(val) replaces f"{val:.16G}"
|
||||
DEPS: None modified
|
||||
INTENT: Fix float precision overflow"""
|
||||
INTENT: Fix precision while maintaining FITS compliance"""
|
||||
|
||||
prompt + '\n\n'
|
||||
|
||||
|
||||
@@ -429,14 +429,23 @@ async def resolve_issue(
|
||||
# checkout the repo
|
||||
repo_dir = os.path.join(output_dir, 'repo')
|
||||
if not os.path.exists(repo_dir):
|
||||
checkout_output = subprocess.check_output(
|
||||
[
|
||||
'git',
|
||||
'clone',
|
||||
issue_handler.get_clone_url(),
|
||||
f'{output_dir}/repo',
|
||||
]
|
||||
).decode('utf-8')
|
||||
# Configure Git LFS to skip downloading large files if requested
|
||||
if os.getenv('GIT_LFS_SKIP_SMUDGE') == '1':
|
||||
subprocess.check_output(['git', 'config', '--global', 'filter.lfs.smudge', 'git-lfs smudge --skip'])
|
||||
subprocess.check_output(['git', 'config', '--global', 'filter.lfs.process', 'git-lfs filter-process --skip'])
|
||||
|
||||
# Build git clone command
|
||||
clone_cmd = ['git', 'clone']
|
||||
|
||||
# Add --depth if requested
|
||||
if depth := os.getenv('GIT_CLONE_DEPTH'):
|
||||
clone_cmd.extend(['--depth', depth])
|
||||
|
||||
# Add repository URL and destination
|
||||
clone_cmd.extend([issue_handler.get_clone_url(), f'{output_dir}/repo'])
|
||||
|
||||
# Execute git clone
|
||||
checkout_output = subprocess.check_output(clone_cmd).decode('utf-8')
|
||||
if 'fatal' in checkout_output:
|
||||
raise RuntimeError(f'Failed to clone repository: {checkout_output}')
|
||||
|
||||
|
||||
@@ -254,6 +254,9 @@ class Runtime(FileEditRuntimeMixin):
|
||||
|
||||
# this might be unnecessary, since source should be set by the event stream when we're here
|
||||
source = event.source if event.source else EventSource.AGENT
|
||||
if isinstance(observation, NullObservation):
|
||||
# don't add null observations to the event stream
|
||||
return
|
||||
self.event_stream.add_event(observation, source) # type: ignore[arg-type]
|
||||
|
||||
def clone_repo(
|
||||
|
||||
@@ -153,6 +153,12 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
return False
|
||||
self.log('debug', f'Error while looking for remote runtime: {e}')
|
||||
raise
|
||||
except requests.exceptions.JSONDecodeError as e:
|
||||
self.log(
|
||||
'error',
|
||||
f'Invalid JSON response from runtime API: {e}. URL: {self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}. Response: {response}',
|
||||
)
|
||||
raise
|
||||
|
||||
if status == 'running':
|
||||
return True
|
||||
|
||||
@@ -130,8 +130,9 @@ async def _create_new_conversation(
|
||||
@app.post('/conversations')
|
||||
async def new_conversation(request: Request, data: InitSessionRequest):
|
||||
"""Initialize a new session or join an existing one.
|
||||
|
||||
After successful initialization, the client should connect to the WebSocket
|
||||
using the returned conversation ID
|
||||
using the returned conversation ID.
|
||||
"""
|
||||
logger.info('Initializing new conversation')
|
||||
user_id = get_user_id(request)
|
||||
@@ -188,10 +189,19 @@ async def search_conversations(
|
||||
config, get_user_id(request)
|
||||
)
|
||||
conversation_metadata_result_set = await conversation_store.search(page_id, limit)
|
||||
|
||||
# Filter out conversations older than max_age
|
||||
now = datetime.now(timezone.utc)
|
||||
max_age = config.conversation_max_age_seconds
|
||||
filtered_results = [
|
||||
conversation for conversation in conversation_metadata_result_set.results
|
||||
if hasattr(conversation, 'created_at') and
|
||||
(now - conversation.created_at.replace(tzinfo=timezone.utc)).total_seconds() <= max_age
|
||||
]
|
||||
|
||||
conversation_ids = set(
|
||||
conversation.conversation_id
|
||||
for conversation in conversation_metadata_result_set.results
|
||||
if hasattr(conversation, 'created_at')
|
||||
for conversation in filtered_results
|
||||
)
|
||||
running_conversations = await conversation_manager.get_running_agent_loops(
|
||||
get_user_id(request), set(conversation_ids)
|
||||
@@ -202,7 +212,7 @@ async def search_conversations(
|
||||
conversation=conversation,
|
||||
is_running=conversation.conversation_id in running_conversations,
|
||||
)
|
||||
for conversation in conversation_metadata_result_set.results
|
||||
for conversation in filtered_results
|
||||
),
|
||||
next_page_id=conversation_metadata_result_set.next_page_id,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from openhands.core.cli import read_input
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.io import read_input
|
||||
|
||||
|
||||
def test_single_line_input():
|
||||
@@ -10,7 +10,7 @@ def test_single_line_input():
|
||||
config.cli_multiline_input = False
|
||||
|
||||
with patch('builtins.input', return_value='hello world'):
|
||||
result = read_input(config)
|
||||
result = read_input(config.cli_multiline_input)
|
||||
assert result == 'hello world'
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ def test_multiline_input():
|
||||
mock_inputs = ['line 1', 'line 2', 'line 3', '/exit']
|
||||
|
||||
with patch('builtins.input', side_effect=mock_inputs):
|
||||
result = read_input(config)
|
||||
result = read_input(config.cli_multiline_input)
|
||||
assert result == 'line 1\nline 2\nline 3'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
|
||||
from openhands.server.routes.manage_conversations import (
|
||||
delete_conversation,
|
||||
get_conversation,
|
||||
@@ -30,8 +31,8 @@ def _patch_store():
|
||||
'selected_repository': 'foobar',
|
||||
'conversation_id': 'some_conversation_id',
|
||||
'github_user_id': '12345',
|
||||
'created_at': '2025-01-01T00:00:00',
|
||||
'last_updated_at': '2025-01-01T00:01:00',
|
||||
'created_at': '2025-01-01T00:00:00+00:00',
|
||||
'last_updated_at': '2025-01-01T00:01:00+00:00',
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -49,22 +50,46 @@ def _patch_store():
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_conversations():
|
||||
with _patch_store():
|
||||
result_set = await search_conversations(
|
||||
MagicMock(state=MagicMock(github_token=''))
|
||||
)
|
||||
expected = ConversationInfoResultSet(
|
||||
results=[
|
||||
ConversationInfo(
|
||||
conversation_id='some_conversation_id',
|
||||
title='Some Conversation',
|
||||
created_at=datetime.fromisoformat('2025-01-01T00:00:00'),
|
||||
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'),
|
||||
status=ConversationStatus.STOPPED,
|
||||
selected_repository='foobar',
|
||||
)
|
||||
]
|
||||
)
|
||||
assert result_set == expected
|
||||
with patch(
|
||||
'openhands.server.routes.manage_conversations.config'
|
||||
) as mock_config:
|
||||
mock_config.conversation_max_age_seconds = 864000 # 10 days
|
||||
with patch(
|
||||
'openhands.server.routes.manage_conversations.conversation_manager'
|
||||
) as mock_manager:
|
||||
|
||||
async def mock_get_running_agent_loops(*args, **kwargs):
|
||||
return set()
|
||||
|
||||
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
|
||||
with patch(
|
||||
'openhands.server.routes.manage_conversations.datetime'
|
||||
) as mock_datetime:
|
||||
mock_datetime.now.return_value = datetime.fromisoformat(
|
||||
'2025-01-01T00:00:00+00:00'
|
||||
)
|
||||
mock_datetime.fromisoformat = datetime.fromisoformat
|
||||
mock_datetime.timezone = timezone
|
||||
result_set = await search_conversations(
|
||||
MagicMock(state=MagicMock(github_token=''))
|
||||
)
|
||||
expected = ConversationInfoResultSet(
|
||||
results=[
|
||||
ConversationInfo(
|
||||
conversation_id='some_conversation_id',
|
||||
title='Some Conversation',
|
||||
created_at=datetime.fromisoformat(
|
||||
'2025-01-01T00:00:00+00:00'
|
||||
),
|
||||
last_updated_at=datetime.fromisoformat(
|
||||
'2025-01-01T00:01:00+00:00'
|
||||
),
|
||||
status=ConversationStatus.STOPPED,
|
||||
selected_repository='foobar',
|
||||
)
|
||||
]
|
||||
)
|
||||
assert result_set == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -76,8 +101,8 @@ async def test_get_conversation():
|
||||
expected = ConversationInfo(
|
||||
conversation_id='some_conversation_id',
|
||||
title='Some Conversation',
|
||||
created_at=datetime.fromisoformat('2025-01-01T00:00:00'),
|
||||
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'),
|
||||
created_at=datetime.fromisoformat('2025-01-01T00:00:00+00:00'),
|
||||
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00+00:00'),
|
||||
status=ConversationStatus.STOPPED,
|
||||
selected_repository='foobar',
|
||||
)
|
||||
@@ -109,8 +134,8 @@ async def test_update_conversation():
|
||||
expected = ConversationInfo(
|
||||
conversation_id='some_conversation_id',
|
||||
title='New Title',
|
||||
created_at=datetime.fromisoformat('2025-01-01T00:00:00'),
|
||||
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'),
|
||||
created_at=datetime.fromisoformat('2025-01-01T00:00:00+00:00'),
|
||||
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00+00:00'),
|
||||
status=ConversationStatus.STOPPED,
|
||||
selected_repository='foobar',
|
||||
)
|
||||
@@ -120,11 +145,12 @@ async def test_update_conversation():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_conversation():
|
||||
with _patch_store():
|
||||
await delete_conversation(
|
||||
'some_conversation_id',
|
||||
MagicMock(state=MagicMock(github_token='')),
|
||||
)
|
||||
conversation = await get_conversation(
|
||||
'some_conversation_id', MagicMock(state=MagicMock(github_token=''))
|
||||
)
|
||||
assert conversation is None
|
||||
with patch.object(DockerRuntime, 'delete', return_value=None):
|
||||
await delete_conversation(
|
||||
'some_conversation_id',
|
||||
MagicMock(state=MagicMock(github_token='')),
|
||||
)
|
||||
conversation = await get_conversation(
|
||||
'some_conversation_id', MagicMock(state=MagicMock(github_token=''))
|
||||
)
|
||||
assert conversation is None
|
||||
|
||||
91
tests/unit/test_git_lfs.py
Normal file
91
tests/unit/test_git_lfs.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.resolver.resolve_issue import resolve_issue
|
||||
from openhands.resolver.utils import Platform
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_git_lfs_skip_smudge():
|
||||
# Create a temporary directory for the test
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Mock environment variables
|
||||
with mock.patch.dict(os.environ, {'GIT_LFS_SKIP_SMUDGE': '1'}):
|
||||
# Mock subprocess.check_output to verify git config is called
|
||||
with mock.patch('subprocess.check_output') as mock_check_output:
|
||||
# Mock issue handler
|
||||
mock_handler = mock.MagicMock()
|
||||
mock_handler.get_clone_url.return_value = 'https://github.com/test/repo.git'
|
||||
mock_handler.get_converted_issues.return_value = [mock.MagicMock()]
|
||||
|
||||
# Mock issue_handler_factory to return our mock handler
|
||||
with mock.patch('openhands.resolver.resolve_issue.issue_handler_factory', return_value=mock_handler):
|
||||
# Call resolve_issue with test parameters
|
||||
await resolve_issue(
|
||||
owner='test',
|
||||
repo='repo',
|
||||
token='token',
|
||||
username='username',
|
||||
platform=Platform.GITHUB,
|
||||
max_iterations=1,
|
||||
output_dir=temp_dir,
|
||||
llm_config=mock.MagicMock(),
|
||||
runtime_container_image=None,
|
||||
prompt_template='',
|
||||
issue_type='issue',
|
||||
repo_instruction=None,
|
||||
issue_number=1,
|
||||
comment_id=None,
|
||||
)
|
||||
|
||||
# Verify git config was called with correct parameters
|
||||
mock_check_output.assert_any_call(['git', 'config', '--global', 'filter.lfs.smudge', 'git-lfs smudge --skip'])
|
||||
mock_check_output.assert_any_call(['git', 'config', '--global', 'filter.lfs.process', 'git-lfs filter-process --skip'])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_git_clone_depth():
|
||||
# Create a temporary directory for the test
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Mock environment variables
|
||||
with mock.patch.dict(os.environ, {'GIT_CLONE_DEPTH': '1'}):
|
||||
# Mock subprocess.check_output to verify git clone is called with --depth
|
||||
with mock.patch('subprocess.check_output') as mock_check_output:
|
||||
# Mock issue handler
|
||||
mock_handler = mock.MagicMock()
|
||||
mock_handler.get_clone_url.return_value = 'https://github.com/test/repo.git'
|
||||
mock_handler.get_converted_issues.return_value = [mock.MagicMock()]
|
||||
|
||||
# Mock issue_handler_factory to return our mock handler
|
||||
with mock.patch('openhands.resolver.resolve_issue.issue_handler_factory', return_value=mock_handler):
|
||||
# Call resolve_issue with test parameters
|
||||
await resolve_issue(
|
||||
owner='test',
|
||||
repo='repo',
|
||||
token='token',
|
||||
username='username',
|
||||
platform=Platform.GITHUB,
|
||||
max_iterations=1,
|
||||
output_dir=temp_dir,
|
||||
llm_config=mock.MagicMock(),
|
||||
runtime_container_image=None,
|
||||
prompt_template='',
|
||||
issue_type='issue',
|
||||
repo_instruction=None,
|
||||
issue_number=1,
|
||||
comment_id=None,
|
||||
)
|
||||
|
||||
# Verify git clone was called with --depth
|
||||
mock_check_output.assert_any_call([
|
||||
'git',
|
||||
'clone',
|
||||
'--depth',
|
||||
'1',
|
||||
'https://github.com/test/repo.git',
|
||||
f'{temp_dir}/repo',
|
||||
])
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.io import json
|
||||
|
||||
|
||||
def test_event_serialization_deserialization():
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import datetime
|
||||
|
||||
import psutil
|
||||
|
||||
from openhands.core.utils.json import dumps
|
||||
from openhands.io.json import dumps
|
||||
|
||||
|
||||
def get_memory_usage():
|
||||
|
||||
@@ -2,11 +2,11 @@ import pytest
|
||||
|
||||
from openhands.agenthub.micro.agent import parse_response as parse_response_micro
|
||||
from openhands.core.exceptions import LLMResponseError
|
||||
from openhands.core.utils.json import loads as custom_loads
|
||||
from openhands.events.action import (
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.io import loads as custom_loads
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user