mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
29 Commits
openhands-
...
openhands-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87e6fed99c | ||
|
|
8488dd2a03 | ||
|
|
d16842f413 | ||
|
|
9cdb8d06c0 | ||
|
|
3297e4d5a8 | ||
|
|
f9d052c493 | ||
|
|
dc3e43b999 | ||
|
|
8bd2205258 | ||
|
|
6ae84bf992 | ||
|
|
afea9f4bec | ||
|
|
8b1a7dff7e | ||
|
|
5e3123964f | ||
|
|
1ffd66f62e | ||
|
|
b04ec03062 | ||
|
|
ee8438cd59 | ||
|
|
7071742d4a | ||
|
|
d76e83b55e | ||
|
|
239619a0a1 | ||
|
|
50478c7d21 | ||
|
|
4998b5de32 | ||
|
|
dd79acdae1 | ||
|
|
b295f5775c | ||
|
|
dabf0ce3af | ||
|
|
09735c7869 | ||
|
|
e0b231092a | ||
|
|
d6a2c4b167 | ||
|
|
6db32025b4 | ||
|
|
fdc00fbca0 | ||
|
|
08b1031666 |
137
.github/workflows/openhands-resolver.yml
vendored
137
.github/workflows/openhands-resolver.yml
vendored
@@ -184,29 +184,32 @@ jobs:
|
||||
});
|
||||
|
||||
- name: Install OpenHands
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == "issue_comment" || "${GITHUB_EVENT_NAME:-}" == "pull_request_review_comment" ]]; then
|
||||
if [[ "${{ github.event.comment.body }}" == *"@openhands-agent-exp"* ]]; then
|
||||
python -m pip install --upgrade pip
|
||||
pip install git+https://github.com/all-hands-ai/openhands.git
|
||||
else
|
||||
python -m pip install --upgrade -r requirements.txt
|
||||
fi
|
||||
elif [[ "${GITHUB_EVENT_NAME:-}" == "pull_request_review" ]]; then
|
||||
if [[ "${{ github.event.review.body }}" == *"@openhands-agent-exp"* ]]; then
|
||||
python -m pip install --upgrade pip
|
||||
pip install git+https://github.com/all-hands-ai/openhands.git
|
||||
else
|
||||
python -m pip install --upgrade -r requirements.txt
|
||||
fi
|
||||
else
|
||||
if [[ "${{ github.event.label.name }}" == "fix-me-experimental" ]]; then
|
||||
python -m pip install --upgrade pip
|
||||
pip install git+https://github.com/all-hands-ai/openhands.git
|
||||
else
|
||||
python -m pip install --upgrade -r requirements.txt
|
||||
fi
|
||||
fi
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const commentBody = `${{ github.event.comment.body || '' }}`.trim();
|
||||
const reviewBody = `${{ github.event.review.body || '' }}`.trim();
|
||||
const labelName = `${{ github.event.label.name || '' }}`.trim();
|
||||
const eventName = `${{ github.event_name }}`.trim();
|
||||
|
||||
// Check conditions
|
||||
const isExperimentalLabel = labelName === "fix-me-experimental";
|
||||
const isIssueCommentExperimental =
|
||||
(eventName === "issue_comment" || eventName === "pull_request_review_comment") &&
|
||||
commentBody.includes("@openhands-agent-exp");
|
||||
const isReviewCommentExperimental =
|
||||
eventName === "pull_request_review" && reviewBody.includes("@openhands-agent-exp");
|
||||
|
||||
// Perform package installation
|
||||
if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) {
|
||||
console.log("Installing experimental OpenHands...");
|
||||
await exec.exec("python -m pip install --upgrade pip");
|
||||
await exec.exec("pip install git+https://github.com/all-hands-ai/openhands.git");
|
||||
} else {
|
||||
console.log("Installing from requirements.txt...");
|
||||
await exec.exec("python -m pip install --upgrade pip");
|
||||
await exec.exec("pip install -r requirements.txt");
|
||||
}
|
||||
|
||||
- name: Attempt to resolve issue
|
||||
env:
|
||||
@@ -265,9 +268,47 @@ jobs:
|
||||
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
|
||||
fi
|
||||
|
||||
- name: Comment on issue
|
||||
# Step leaves comment for when agent is invoked on PR
|
||||
- name: Analyze Push Logs (Updated PR or No Changes) # Skip comment if PR update was successful OR leave comment if the agent made no code changes
|
||||
uses: actions/github-script@v7
|
||||
if: always()
|
||||
env:
|
||||
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
|
||||
with:
|
||||
github-token: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const issueNumber = ${{ env.ISSUE_NUMBER }};
|
||||
let logContent = '';
|
||||
|
||||
try {
|
||||
logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim();
|
||||
} catch (error) {
|
||||
console.error('Error reading pr_result.txt file:', error);
|
||||
}
|
||||
|
||||
const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`;
|
||||
|
||||
// Check logs from send_pull_request.py (pushes code to GitHub)
|
||||
if (logContent.includes("Updated pull request")) {
|
||||
console.log("Updated pull request found. Skipping comment.");
|
||||
process.env.AGENT_RESPONDED = 'true';
|
||||
} else if (logContent.includes(noChangesMessage)) {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.`
|
||||
});
|
||||
process.env.AGENT_RESPONDED = 'true';
|
||||
}
|
||||
|
||||
# Step leaves comment for when agent is invoked on issue
|
||||
- name: Comment on issue # Comment link to either PR or branch created by agent
|
||||
uses: actions/github-script@v7
|
||||
if: always() # Comment on issue even if the previous steps fail
|
||||
env:
|
||||
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
|
||||
with:
|
||||
github-token: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
script: |
|
||||
@@ -277,18 +318,6 @@ jobs:
|
||||
|
||||
let prNumber = '';
|
||||
let branchName = '';
|
||||
let logContent = '';
|
||||
const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`;
|
||||
|
||||
try {
|
||||
if (success){
|
||||
logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim();
|
||||
} else {
|
||||
logContent = fs.readFileSync('/tmp/branch_result.txt', 'utf8').trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading results file:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
if (success) {
|
||||
@@ -300,20 +329,16 @@ jobs:
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
|
||||
if (logContent.includes(noChangesMessage)) {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.`
|
||||
});
|
||||
} else if (success && prNumber) {
|
||||
|
||||
// Check "success" log from resolver output
|
||||
if (success && prNumber) {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.`
|
||||
});
|
||||
process.env.AGENT_RESPONDED = 'true';
|
||||
} else if (!success && branchName) {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
@@ -321,11 +346,21 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
body: `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`
|
||||
});
|
||||
} else {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.`
|
||||
});
|
||||
process.env.AGENT_RESPONDED = 'true';
|
||||
}
|
||||
|
||||
# Leave error comment when both PR/Issue comment handling fail
|
||||
- name: Fallback Error Comment
|
||||
uses: actions/github-script@v7
|
||||
if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps
|
||||
with:
|
||||
github-token: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
script: |
|
||||
const issueNumber = ${{ env.ISSUE_NUMBER }};
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.`
|
||||
});
|
||||
|
||||
@@ -154,6 +154,10 @@ model = "gpt-4o"
|
||||
# Drop any unmapped (unsupported) params without causing an exception
|
||||
#drop_params = false
|
||||
|
||||
# Modify params for litellm to do transformations like adding a default message, when a message is empty.
|
||||
# Note: this setting is global, unlike drop_params, it cannot be overridden in each call to litellm.
|
||||
#modify_params = true
|
||||
|
||||
# Using the prompt caching feature if provided by the LLM and supported
|
||||
#caching_prompt = true
|
||||
|
||||
|
||||
@@ -202,6 +202,9 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -307,6 +307,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
@@ -279,6 +279,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
@@ -328,6 +328,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
@@ -456,6 +456,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
@@ -571,6 +571,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
llm_config.log_completions = True
|
||||
|
||||
if llm_config is None:
|
||||
|
||||
@@ -466,6 +466,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -238,6 +238,9 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -326,6 +326,9 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -285,6 +285,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -288,6 +288,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -231,6 +231,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -279,6 +279,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -124,6 +124,9 @@ if __name__ == '__main__':
|
||||
# for details of how to set `llm_config`
|
||||
if args.llm_config:
|
||||
specified_llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
specified_llm_config.modify_params = False
|
||||
|
||||
if specified_llm_config:
|
||||
config.llm = specified_llm_config
|
||||
logger.info(f'Config for evaluation: {config}')
|
||||
|
||||
@@ -292,6 +292,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -272,6 +272,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import toml
|
||||
from datasets import load_dataset
|
||||
|
||||
import openhands.agenthub
|
||||
|
||||
from evaluation.utils.shared import (
|
||||
EvalException,
|
||||
EvalMetadata,
|
||||
@@ -76,7 +75,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
|
||||
'5. Think about edgecases and make sure your fix handles them as well\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
)
|
||||
)
|
||||
|
||||
if RUN_WITH_BROWSING:
|
||||
instruction += (
|
||||
@@ -491,6 +490,8 @@ if __name__ == '__main__':
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
llm_config.log_completions = True
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
@@ -33,7 +33,7 @@ if [ -d /workspace/$WORKSPACE_NAME ]; then
|
||||
rm -rf /workspace/$WORKSPACE_NAME
|
||||
fi
|
||||
mkdir -p /workspace
|
||||
mv /testbed /workspace/$WORKSPACE_NAME
|
||||
cp -r /testbed /workspace/$WORKSPACE_NAME
|
||||
|
||||
# Activate instance-specific environment
|
||||
. /opt/miniconda3/etc/profile.d/conda.sh
|
||||
|
||||
@@ -181,6 +181,9 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -212,6 +212,8 @@ if __name__ == '__main__':
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
|
||||
@@ -51,6 +51,22 @@ describe("ChatInput", () => {
|
||||
expect(onSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not call onSubmit when the message is only whitespace", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ChatInput onSubmit={onSubmitMock} />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
|
||||
await user.type(textarea, " ");
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(onSubmitMock).not.toHaveBeenCalled();
|
||||
|
||||
await user.type(textarea, " \t\n");
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(onSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should disable submit", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ChatInput disabled onSubmit={onSubmitMock} />);
|
||||
|
||||
@@ -2,12 +2,28 @@ import { describe, expect, it } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual('react-i18next');
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key:string) => key,
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
language: 'en',
|
||||
exists: () => true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
describe("ExpandableMessage", () => {
|
||||
it("should render with neutral border for non-action messages", () => {
|
||||
renderWithProviders(<ExpandableMessage message="Hello" type="thought" />);
|
||||
const element = screen.getByText("Hello");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-start");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -15,21 +31,22 @@ describe("ExpandableMessage", () => {
|
||||
it("should render with neutral border for error messages", () => {
|
||||
renderWithProviders(<ExpandableMessage message="Error occurred" type="error" />);
|
||||
const element = screen.getByText("Error occurred");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-start");
|
||||
expect(container).toHaveClass("border-danger");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with success icon for successful action messages", () => {
|
||||
renderWithProviders(
|
||||
<ExpandableMessage
|
||||
id="OBSERVATION_MESSAGE$RUN"
|
||||
message="Command executed successfully"
|
||||
type="action"
|
||||
success={true}
|
||||
/>
|
||||
);
|
||||
const element = screen.getByText("Command executed successfully");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-start");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
const icon = screen.getByTestId("status-icon");
|
||||
expect(icon).toHaveClass("fill-success");
|
||||
@@ -38,22 +55,29 @@ describe("ExpandableMessage", () => {
|
||||
it("should render with error icon for failed action messages", () => {
|
||||
renderWithProviders(
|
||||
<ExpandableMessage
|
||||
id="OBSERVATION_MESSAGE$RUN"
|
||||
message="Command failed"
|
||||
type="action"
|
||||
success={false}
|
||||
/>
|
||||
);
|
||||
const element = screen.getByText("Command failed");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-start");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
const icon = screen.getByTestId("status-icon");
|
||||
expect(icon).toHaveClass("fill-danger");
|
||||
});
|
||||
|
||||
it("should render with neutral border and no icon for action messages without success prop", () => {
|
||||
renderWithProviders(<ExpandableMessage message="Running command" type="action" />);
|
||||
const element = screen.getByText("Running command");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
renderWithProviders(
|
||||
<ExpandableMessage
|
||||
id="OBSERVATION_MESSAGE$RUN"
|
||||
message="Running command"
|
||||
type="action"
|
||||
/>
|
||||
);
|
||||
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-start");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
45
frontend/__tests__/components/jupyter/jupyter.test.tsx
Normal file
45
frontend/__tests__/components/jupyter/jupyter.test.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { JupyterEditor } from "#/components/features/jupyter/jupyter";
|
||||
import { jupyterReducer } from "#/state/jupyter-slice";
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
|
||||
describe("JupyterEditor", () => {
|
||||
const mockStore = configureStore({
|
||||
reducer: {
|
||||
fileState: () => ({}),
|
||||
initalQuery: () => ({}),
|
||||
browser: () => ({}),
|
||||
chat: () => ({}),
|
||||
code: () => ({}),
|
||||
cmd: () => ({}),
|
||||
agent: () => ({}),
|
||||
jupyter: jupyterReducer,
|
||||
securityAnalyzer: () => ({}),
|
||||
status: () => ({}),
|
||||
},
|
||||
preloadedState: {
|
||||
jupyter: {
|
||||
cells: Array(20).fill({
|
||||
content: "Test cell content",
|
||||
type: "input",
|
||||
output: "Test output",
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("should have a scrollable container", () => {
|
||||
render(
|
||||
<Provider store={mockStore}>
|
||||
<div style={{ height: "100vh" }}>
|
||||
<JupyterEditor maxWidth={800} />
|
||||
</div>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId("jupyter-container");
|
||||
expect(container).toHaveClass("flex-1 overflow-y-auto");
|
||||
});
|
||||
});
|
||||
@@ -4,26 +4,6 @@ import { vi, describe, afterEach, it, expect } from "vitest";
|
||||
import { Command, appendInput, appendOutput } from "#/state/command-slice";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockTerminal = {
|
||||
open: vi.fn(),
|
||||
write: vi.fn(),
|
||||
writeln: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
onKey: vi.fn(),
|
||||
attachCustomKeyEventHandler: vi.fn(),
|
||||
loadAddon: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("@xterm/xterm", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@xterm/xterm")>()),
|
||||
Terminal: vi.fn().mockImplementation(() => mockTerminal),
|
||||
}));
|
||||
|
||||
const renderTerminal = (commands: Command[] = []) =>
|
||||
renderWithProviders(<Terminal secrets={[]} />, {
|
||||
preloadedState: {
|
||||
@@ -34,6 +14,26 @@ const renderTerminal = (commands: Command[] = []) =>
|
||||
});
|
||||
|
||||
describe.skip("Terminal", () => {
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockTerminal = {
|
||||
open: vi.fn(),
|
||||
write: vi.fn(),
|
||||
writeln: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
onKey: vi.fn(),
|
||||
attachCustomKeyEventHandler: vi.fn(),
|
||||
loadAddon: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("@xterm/xterm", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@xterm/xterm")>()),
|
||||
Terminal: vi.fn().mockImplementation(() => mockTerminal),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ReactNode } from "react";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
import { Command } from "#/state/command-slice";
|
||||
|
||||
|
||||
interface TestTerminalComponentProps {
|
||||
commands: Command[];
|
||||
secrets: string[];
|
||||
@@ -15,7 +14,7 @@ function TestTerminalComponent({
|
||||
commands,
|
||||
secrets,
|
||||
}: TestTerminalComponentProps) {
|
||||
const ref = useTerminal(commands, secrets);
|
||||
const ref = useTerminal({ commands, secrets, disabled: false });
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
@@ -24,9 +23,7 @@ interface WrapperProps {
|
||||
}
|
||||
|
||||
function Wrapper({ children }: WrapperProps) {
|
||||
return (
|
||||
<div>{children}</div>
|
||||
)
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
describe("useTerminal", () => {
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.15.2",
|
||||
"version": "0.15.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.15.2",
|
||||
"version": "0.15.3",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.15.2",
|
||||
"version": "0.15.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"APP_MODE": "oss",
|
||||
"GITHUB_CLIENT_ID": "",
|
||||
"POSTHOG_CLIENT_KEY": "phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from "axios";
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
const github = axios.create({
|
||||
baseURL: "https://api.github.com",
|
||||
@@ -18,4 +18,86 @@ const removeAuthTokenHeader = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { github, setAuthTokenHeader, removeAuthTokenHeader };
|
||||
/**
|
||||
* Checks if response has attributes to perform refresh
|
||||
*/
|
||||
const canRefresh = (error: unknown): boolean =>
|
||||
!!(
|
||||
error instanceof AxiosError &&
|
||||
error.config &&
|
||||
error.response &&
|
||||
error.response.status
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if the data is a GitHub error response
|
||||
* @param data The data to check
|
||||
* @returns Boolean indicating if the data is a GitHub error response
|
||||
*/
|
||||
export const isGitHubErrorReponse = <T extends object | Array<unknown>>(
|
||||
data: T | GitHubErrorReponse | null,
|
||||
): data is GitHubErrorReponse =>
|
||||
!!data && "message" in data && data.message !== undefined;
|
||||
|
||||
// Axios interceptor to handle token refresh
|
||||
const setupAxiosInterceptors = (
|
||||
refreshToken: () => Promise<boolean>,
|
||||
logout: () => void,
|
||||
) => {
|
||||
github.interceptors.response.use(
|
||||
// Pass successful responses through
|
||||
(response) => {
|
||||
const parsedData = response.data;
|
||||
if (isGitHubErrorReponse(parsedData)) {
|
||||
const error = new AxiosError(
|
||||
"Failed",
|
||||
"",
|
||||
response.config,
|
||||
response.request,
|
||||
response,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
// Retry request exactly once if token is expired
|
||||
async (error) => {
|
||||
if (!canRefresh(error)) {
|
||||
return Promise.reject(new Error("Failed to refresh token"));
|
||||
}
|
||||
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Check if the error is due to an expired token
|
||||
if (
|
||||
error.response.status === 401 &&
|
||||
!originalRequest._retry // Prevent infinite retry loops
|
||||
) {
|
||||
originalRequest._retry = true;
|
||||
try {
|
||||
const refreshed = await refreshToken();
|
||||
if (refreshed) {
|
||||
return await github(originalRequest);
|
||||
}
|
||||
|
||||
logout();
|
||||
return await Promise.reject(new Error("Failed to refresh token"));
|
||||
} catch (refreshError) {
|
||||
// If token refresh fails, evict the user
|
||||
logout();
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
// If the error is not due to an expired token, propagate the error
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
github,
|
||||
setAuthTokenHeader,
|
||||
removeAuthTokenHeader,
|
||||
setupAxiosInterceptors,
|
||||
};
|
||||
|
||||
@@ -1,42 +1,81 @@
|
||||
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
|
||||
import { github } from "./github-axios-instance";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
|
||||
/**
|
||||
* Checks if the data is a GitHub error response
|
||||
* @param data The data to check
|
||||
* @returns Boolean indicating if the data is a GitHub error response
|
||||
* Given the user, retrieves app installations IDs for OpenHands Github App
|
||||
* Uses user access token for Github App
|
||||
*/
|
||||
export const isGitHubErrorReponse = <T extends object | Array<unknown>>(
|
||||
data: T | GitHubErrorReponse | null,
|
||||
): data is GitHubErrorReponse =>
|
||||
!!data && "message" in data && data.message !== undefined;
|
||||
export const retrieveGitHubAppInstallations = async (): Promise<number[]> => {
|
||||
const response = await github.get<GithubAppInstallation>(
|
||||
"/user/installations",
|
||||
);
|
||||
|
||||
return response.data.installations.map((installation) => installation.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a GitHub token, retrieves the repositories of the authenticated user
|
||||
* @param token The GitHub token
|
||||
* @returns A list of repositories or an error response
|
||||
* Retrieves repositories where OpenHands Github App has been installed
|
||||
* @param installationIndex Pagination cursor position for app installation IDs
|
||||
* @param installations Collection of all App installation IDs for OpenHands Github App
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
export const retrieveGitHubAppRepositories = async (
|
||||
installationIndex: number,
|
||||
installations: number[],
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) => {
|
||||
const installationId = installations[installationIndex];
|
||||
const response = await openHands.get<GitHubAppRepository>(
|
||||
"/api/github/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
installation_id: installationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const link = response.headers.link ?? "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
let nextInstallation: number | null;
|
||||
|
||||
if (nextPage) {
|
||||
nextInstallation = installationIndex;
|
||||
} else if (installationIndex + 1 < installations.length) {
|
||||
nextInstallation = installationIndex + 1;
|
||||
} else {
|
||||
nextInstallation = null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data.repositories,
|
||||
nextPage,
|
||||
installationIndex: nextInstallation,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a PAT, retrieves the repositories of the user
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
export const retrieveGitHubUserRepositories = async (
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) => {
|
||||
const response = await github.get<GitHubRepository[]>("/user/repos", {
|
||||
params: {
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
const response = await openHands.get<GitHubRepository[]>(
|
||||
"/api/github/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
},
|
||||
},
|
||||
transformResponse: (data) => {
|
||||
const parsedData: GitHubRepository[] | GitHubErrorReponse =
|
||||
JSON.parse(data);
|
||||
|
||||
if (isGitHubErrorReponse(parsedData)) {
|
||||
throw new Error(parsedData.message);
|
||||
}
|
||||
|
||||
return parsedData;
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const link = response.headers.link ?? "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
@@ -46,21 +85,10 @@ export const retrieveGitHubUserRepositories = async (
|
||||
|
||||
/**
|
||||
* Given a GitHub token, retrieves the authenticated user
|
||||
* @param token The GitHub token
|
||||
* @returns The authenticated user or an error response
|
||||
*/
|
||||
export const retrieveGitHubUser = async () => {
|
||||
const response = await github.get<GitHubUser>("/user", {
|
||||
transformResponse: (data) => {
|
||||
const parsedData: GitHubUser | GitHubErrorReponse = JSON.parse(data);
|
||||
|
||||
if (isGitHubErrorReponse(parsedData)) {
|
||||
throw new Error(parsedData.message);
|
||||
}
|
||||
|
||||
return parsedData;
|
||||
},
|
||||
});
|
||||
const response = await github.get<GitHubUser>("/user");
|
||||
|
||||
const { data } = response;
|
||||
|
||||
@@ -79,24 +107,14 @@ export const retrieveGitHubUser = async () => {
|
||||
export const retrieveLatestGitHubCommit = async (
|
||||
repository: string,
|
||||
): Promise<GitHubCommit> => {
|
||||
const response = await github.get<GitHubCommit>(
|
||||
const response = await github.get<GitHubCommit[]>(
|
||||
`/repos/${repository}/commits`,
|
||||
{
|
||||
params: {
|
||||
per_page: 1,
|
||||
},
|
||||
transformResponse: (data) => {
|
||||
const parsedData: GitHubCommit[] | GitHubErrorReponse =
|
||||
JSON.parse(data);
|
||||
|
||||
if (isGitHubErrorReponse(parsedData)) {
|
||||
throw new Error(parsedData.message);
|
||||
}
|
||||
|
||||
return parsedData[0];
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return response.data[0];
|
||||
};
|
||||
|
||||
@@ -42,7 +42,9 @@ class OpenHands {
|
||||
}
|
||||
|
||||
static async getConfig(): Promise<GetConfigResponse> {
|
||||
const { data } = await openHands.get<GetConfigResponse>("/config.json");
|
||||
const { data } = await openHands.get<GetConfigResponse>(
|
||||
"/api/options/config",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -136,6 +138,20 @@ class OpenHands {
|
||||
return response.status === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Github Token
|
||||
* @returns Refreshed Github access token
|
||||
*/
|
||||
static async refreshToken(
|
||||
appMode: GetConfigResponse["APP_MODE"],
|
||||
): Promise<string> {
|
||||
if (appMode === "oss") return "";
|
||||
|
||||
const response =
|
||||
await openHands.post<GitHubAccessTokenResponse>("/api/refresh-token");
|
||||
return response.data.access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blob of the workspace zip
|
||||
* @returns Blob of the workspace zip
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface Feedback {
|
||||
|
||||
export interface GetConfigResponse {
|
||||
APP_MODE: "saas" | "oss";
|
||||
APP_SLUG?: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
POSTHOG_CLIENT_KEY: string;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function ChatInput({
|
||||
|
||||
const handleSubmitMessage = () => {
|
||||
const message = value || textareaRef.current?.value || "";
|
||||
if (message) {
|
||||
if (message.trim()) {
|
||||
onSubmit(message);
|
||||
onChange?.("");
|
||||
if (textareaRef.current) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import ArrowUp from "#/icons/angle-up-solid.svg?react";
|
||||
import ArrowDown from "#/icons/angle-down-solid.svg?react";
|
||||
import CheckCircle from "#/icons/check-circle-solid.svg?react";
|
||||
import XCircle from "#/icons/x-circle-solid.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ExpandableMessageProps {
|
||||
id?: string;
|
||||
@@ -35,27 +36,63 @@ export function ExpandableMessage({
|
||||
}
|
||||
}, [id, message, i18n.language]);
|
||||
|
||||
const arrowClasses = "h-4 w-4 ml-2 inline fill-neutral-300";
|
||||
const statusIconClasses = "h-4 w-4 ml-2 inline";
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center justify-between border-l-2 border-neutral-300 pl-2 my-2 py-2">
|
||||
<div className="text-sm leading-4 flex flex-col gap-2 max-w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2",
|
||||
type === "error" ? "border-danger" : "border-neutral-300",
|
||||
)}
|
||||
>
|
||||
<div className="text-sm w-full">
|
||||
{headline && (
|
||||
<p className="text-neutral-300 font-bold">
|
||||
{headline}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="cursor-pointer text-left"
|
||||
>
|
||||
{showDetails ? (
|
||||
<ArrowUp className={arrowClasses} />
|
||||
) : (
|
||||
<ArrowDown className={arrowClasses} />
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
<span
|
||||
className={cn(
|
||||
"font-bold",
|
||||
type === "error" ? "text-danger" : "text-neutral-300",
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
>
|
||||
{headline}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="cursor-pointer text-left"
|
||||
>
|
||||
{showDetails ? (
|
||||
<ArrowUp
|
||||
className={cn(
|
||||
"h-4 w-4 ml-2 inline",
|
||||
type === "error" ? "fill-danger" : "fill-neutral-300",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<ArrowDown
|
||||
className={cn(
|
||||
"h-4 w-4 ml-2 inline",
|
||||
type === "error" ? "fill-danger" : "fill-neutral-300",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
{type === "action" && success !== undefined && (
|
||||
<span className="flex-shrink-0">
|
||||
{success ? (
|
||||
<CheckCircle
|
||||
data-testid="status-icon"
|
||||
className={cn(statusIconClasses, "fill-success")}
|
||||
/>
|
||||
) : (
|
||||
<XCircle
|
||||
data-testid="status-icon"
|
||||
className={cn(statusIconClasses, "fill-danger")}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showDetails && (
|
||||
<Markdown
|
||||
@@ -71,21 +108,6 @@ export function ExpandableMessage({
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
{type === "action" && success !== undefined && (
|
||||
<div className="flex-shrink-0">
|
||||
{success ? (
|
||||
<CheckCircle
|
||||
data-testid="status-icon"
|
||||
className={`${statusIconClasses} fill-success`}
|
||||
/>
|
||||
) : (
|
||||
<XCircle
|
||||
data-testid="status-icon"
|
||||
className={`${statusIconClasses} fill-danger`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import { FileExplorerHeader } from "./file-explorer-header";
|
||||
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
|
||||
import { OpenVSCodeButton } from "#/components/shared/buttons/open-vscode-button";
|
||||
import { addAssistantMessage } from "#/state/chat-slice";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
|
||||
interface FileExplorerProps {
|
||||
isOpen: boolean;
|
||||
@@ -22,6 +26,7 @@ interface FileExplorerProps {
|
||||
}
|
||||
|
||||
export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
const { status } = useWsClient();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -30,12 +35,11 @@ export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const agentIsReady =
|
||||
curAgentState !== AgentState.INIT && curAgentState !== AgentState.LOADING;
|
||||
|
||||
const { data: paths, refetch, error } = useListFiles();
|
||||
const { mutate: uploadFiles } = useUploadFiles();
|
||||
const { data: vscodeUrl } = useVSCodeUrl({ enabled: agentIsReady });
|
||||
const { data: vscodeUrl } = useVSCodeUrl({
|
||||
enabled: status === WsClientProviderStatus.ACTIVE,
|
||||
});
|
||||
|
||||
const handleOpenVSCode = () => {
|
||||
if (vscodeUrl?.vscode_url) {
|
||||
@@ -166,7 +170,7 @@ export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
{isOpen && (
|
||||
<OpenVSCodeButton
|
||||
onClick={handleOpenVSCode}
|
||||
isDisabled={!agentIsReady}
|
||||
isDisabled={status === WsClientProviderStatus.OPENING}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Autocomplete, AutocompleteItem } from "@nextui-org/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { setSelectedRepository } from "#/state/initial-query-slice";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
interface GitHubRepositorySelectorProps {
|
||||
onSelect: () => void;
|
||||
@@ -12,11 +13,25 @@ export function GitHubRepositorySelector({
|
||||
onSelect,
|
||||
repositories,
|
||||
}: GitHubRepositorySelectorProps) {
|
||||
const { data: config } = useConfig();
|
||||
|
||||
// Add option to install app onto more repos
|
||||
const finalRepositories =
|
||||
config?.APP_MODE === "saas"
|
||||
? [{ id: -1000, full_name: "Add more repositories..." }, ...repositories]
|
||||
: repositories;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRepoSelection = (id: string | null) => {
|
||||
const repo = repositories.find((r) => r.id.toString() === id);
|
||||
if (repo) {
|
||||
const repo = finalRepositories.find((r) => r.id.toString() === id);
|
||||
if (id === "-1000") {
|
||||
if (config?.APP_SLUG)
|
||||
window.open(
|
||||
`https://github.com/apps/${config.APP_SLUG}/installations/new`,
|
||||
"_blank",
|
||||
);
|
||||
} else if (repo) {
|
||||
// set query param
|
||||
dispatch(setSelectedRepository(repo.full_name));
|
||||
posthog.capture("repository_selected");
|
||||
@@ -29,6 +44,19 @@ export function GitHubRepositorySelector({
|
||||
dispatch(setSelectedRepository(null));
|
||||
};
|
||||
|
||||
const emptyContent = config?.APP_SLUG ? (
|
||||
<a
|
||||
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline"
|
||||
>
|
||||
Add more repositories...
|
||||
</a>
|
||||
) : (
|
||||
"No results found."
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
data-testid="github-repo-selector"
|
||||
@@ -43,8 +71,11 @@ export function GitHubRepositorySelector({
|
||||
}}
|
||||
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
|
||||
clearButtonProps={{ onClick: handleClearSelection }}
|
||||
listboxProps={{
|
||||
emptyContent,
|
||||
}}
|
||||
>
|
||||
{repositories.map((repo) => (
|
||||
{finalRepositories.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import { isGitHubErrorReponse } from "#/api/github";
|
||||
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
import { GitHubRepositorySelector } from "./github-repo-selector";
|
||||
import { ModalButton } from "#/components/shared/buttons/modal-button";
|
||||
import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { isGitHubErrorReponse } from "#/api/github-axios-instance";
|
||||
|
||||
interface GitHubRepositoriesSuggestionBoxProps {
|
||||
handleSubmit: () => void;
|
||||
|
||||
@@ -10,16 +10,17 @@ interface JupyterEditorProps {
|
||||
}
|
||||
|
||||
export function JupyterEditor({ maxWidth }: JupyterEditorProps) {
|
||||
const { cells } = useSelector((state: RootState) => state.jupyter);
|
||||
const cells = useSelector((state: RootState) => state.jupyter?.cells ?? []);
|
||||
const jupyterRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const { hitBottom, scrollDomToBottom, onChatBodyScroll } =
|
||||
useScrollToBottom(jupyterRef);
|
||||
|
||||
return (
|
||||
<div className="flex-1" style={{ maxWidth }}>
|
||||
<div className="flex-1 h-full flex flex-col" style={{ maxWidth }}>
|
||||
<div
|
||||
className="overflow-y-auto h-full"
|
||||
data-testid="jupyter-container"
|
||||
className="flex-1 overflow-y-auto"
|
||||
ref={jupyterRef}
|
||||
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||
>
|
||||
|
||||
@@ -17,19 +17,38 @@ export function code({
|
||||
const match = /language-(\w+)/.exec(className || ""); // get the language
|
||||
|
||||
if (!match) {
|
||||
const isMultiline = String(children).includes("\n");
|
||||
|
||||
if (!isMultiline) {
|
||||
return (
|
||||
<code
|
||||
className={className}
|
||||
style={{
|
||||
backgroundColor: "#2a3038",
|
||||
padding: "0.2em 0.4em",
|
||||
borderRadius: "4px",
|
||||
color: "#e6edf3",
|
||||
border: "1px solid #30363d",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code
|
||||
className={className}
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#2a3038",
|
||||
padding: "0.2em 0.4em",
|
||||
padding: "1em",
|
||||
borderRadius: "4px",
|
||||
color: "#e6edf3",
|
||||
border: "1px solid #30363d",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
<code className={className}>{String(children).replace(/\n$/, "")}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
export function TerminalStatusLabel() {
|
||||
const { status } = useWsClient();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
status === WsClientProviderStatus.ACTIVE && "bg-green-500",
|
||||
status !== WsClientProviderStatus.ACTIVE &&
|
||||
"bg-red-500 animate-pulse",
|
||||
)}
|
||||
/>
|
||||
Terminal
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,25 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
|
||||
interface TerminalProps {
|
||||
secrets: string[];
|
||||
}
|
||||
|
||||
function Terminal({ secrets }: TerminalProps) {
|
||||
const { status } = useWsClient();
|
||||
const { commands } = useSelector((state: RootState) => state.cmd);
|
||||
const ref = useTerminal(commands, secrets);
|
||||
|
||||
const ref = useTerminal({
|
||||
commands,
|
||||
secrets,
|
||||
disabled: status === WsClientProviderStatus.OPENING,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full p-2 min-h-0">
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { NavTab } from "./nav-tab";
|
||||
|
||||
interface ContainerProps {
|
||||
label?: string;
|
||||
label?: React.ReactNode;
|
||||
labels?: {
|
||||
label: string | React.ReactNode;
|
||||
to: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { ModalButton } from "../../buttons/modal-button";
|
||||
import { CustomInput } from "../../custom-input";
|
||||
import { FormFieldset } from "../../form-fieldset";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
interface AccountSettingsFormProps {
|
||||
onClose: () => void;
|
||||
@@ -28,6 +29,7 @@ export function AccountSettingsForm({
|
||||
analyticsConsent,
|
||||
}: AccountSettingsFormProps) {
|
||||
const { gitHubToken, setGitHubToken, logout } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { saveSettings } = useUserPrefs();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -64,6 +66,16 @@ export function AccountSettingsForm({
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<BaseModalTitle title="Account Settings" />
|
||||
|
||||
{config?.APP_MODE === "saas" && config?.APP_SLUG && (
|
||||
<a
|
||||
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline"
|
||||
>
|
||||
Configure Github Repositories
|
||||
</a>
|
||||
)}
|
||||
<FormFieldset
|
||||
id="language"
|
||||
label="Language"
|
||||
@@ -75,23 +87,27 @@ export function AccountSettingsForm({
|
||||
}))}
|
||||
/>
|
||||
|
||||
<CustomInput
|
||||
name="ghToken"
|
||||
label="GitHub Token"
|
||||
type="password"
|
||||
defaultValue={gitHubToken ?? ""}
|
||||
/>
|
||||
<BaseModalDescription>
|
||||
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN)}{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="text-[#791B80] underline"
|
||||
>
|
||||
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$HERE)}
|
||||
</a>
|
||||
</BaseModalDescription>
|
||||
{config?.APP_MODE !== "saas" && (
|
||||
<>
|
||||
<CustomInput
|
||||
name="ghToken"
|
||||
label="GitHub Token"
|
||||
type="password"
|
||||
defaultValue={gitHubToken ?? ""}
|
||||
/>
|
||||
<BaseModalDescription>
|
||||
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN)}{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="text-[#791B80] underline"
|
||||
>
|
||||
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$HERE)}
|
||||
</a>
|
||||
</BaseModalDescription>
|
||||
</>
|
||||
)}
|
||||
{gitHubError && (
|
||||
<p className="text-danger text-xs">
|
||||
{t(I18nKey.ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID)}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { CustomModelInput } from "../../inputs/custom-model-input";
|
||||
import { SecurityAnalyzerInput } from "../../inputs/security-analyzers-input";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
interface SettingsFormProps {
|
||||
disabled?: boolean;
|
||||
@@ -44,6 +45,7 @@ export function SettingsForm({
|
||||
}: SettingsFormProps) {
|
||||
const { saveSettings } = useUserPrefs();
|
||||
const endSession = useEndSession();
|
||||
const { logout } = useAuth();
|
||||
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
@@ -96,9 +98,9 @@ export function SettingsForm({
|
||||
const isUsingAdvancedOptions = keys.includes("use-advanced-options");
|
||||
const newSettings = extractSettings(formData);
|
||||
|
||||
saveSettings(newSettings);
|
||||
saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic");
|
||||
updateSettingsVersion();
|
||||
updateSettingsVersion(logout);
|
||||
saveSettings(newSettings);
|
||||
resetOngoingSession();
|
||||
|
||||
posthog.capture("settings_saved", {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import {
|
||||
removeAuthTokenHeader as removeOpenHandsAuthTokenHeader,
|
||||
removeGitHubTokenHeader as removeOpenHandsGitHubTokenHeader,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
import {
|
||||
setAuthTokenHeader as setGitHubAuthTokenHeader,
|
||||
removeAuthTokenHeader as removeGitHubAuthTokenHeader,
|
||||
setupAxiosInterceptors as setupGithubAxiosInterceptors,
|
||||
} from "#/api/github-axios-instance";
|
||||
|
||||
interface AuthContextType {
|
||||
@@ -18,6 +20,7 @@ interface AuthContextType {
|
||||
setGitHubToken: (token: string | null) => void;
|
||||
clearToken: () => void;
|
||||
clearGitHubToken: () => void;
|
||||
refreshToken: () => Promise<boolean>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
@@ -69,19 +72,37 @@ function AuthProvider({ children }: React.PropsWithChildren) {
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
clearGitHubToken();
|
||||
posthog.reset();
|
||||
};
|
||||
|
||||
const refreshToken = async (): Promise<boolean> => {
|
||||
const config = await OpenHands.getConfig();
|
||||
|
||||
if (config.APP_MODE !== "saas" || !gitHubTokenState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newToken = await OpenHands.refreshToken(config.APP_MODE);
|
||||
if (newToken) {
|
||||
setGitHubToken(newToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
clearGitHubToken();
|
||||
return false;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const storedToken = localStorage.getItem("token");
|
||||
const storedGitHubToken = localStorage.getItem("ghToken");
|
||||
|
||||
setToken(storedToken);
|
||||
setGitHubToken(storedGitHubToken);
|
||||
setupGithubAxiosInterceptors(refreshToken, logout);
|
||||
}, []);
|
||||
|
||||
const logout = () => {
|
||||
clearGitHubToken();
|
||||
posthog.reset();
|
||||
};
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
token: tokenState,
|
||||
@@ -90,6 +111,7 @@ function AuthProvider({ children }: React.PropsWithChildren) {
|
||||
setGitHubToken,
|
||||
clearToken,
|
||||
clearGitHubToken,
|
||||
refreshToken,
|
||||
logout,
|
||||
}),
|
||||
[tokenState, gitHubTokenState],
|
||||
|
||||
@@ -4,9 +4,9 @@ import { io, Socket } from "socket.io-client";
|
||||
import { Settings } from "#/services/settings";
|
||||
import ActionType from "#/types/action-type";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
import AgentState from "#/types/agent-state";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
import { useRate } from "#/hooks/use-rate";
|
||||
import AgentState from "#/types/agent-state";
|
||||
|
||||
const isOpenHandsMessage = (event: Record<string, unknown>) =>
|
||||
event.action === "message";
|
||||
@@ -102,10 +102,12 @@ export function WsClientProvider({
|
||||
if (!Number.isNaN(parseInt(event.id as string, 10))) {
|
||||
lastEventRef.current = event;
|
||||
}
|
||||
|
||||
const extras = event.extras as Record<string, unknown>;
|
||||
if (extras?.agent_state === AgentState.INIT) {
|
||||
setStatus(WsClientProviderStatus.ACTIVE);
|
||||
}
|
||||
|
||||
if (
|
||||
status !== WsClientProviderStatus.ACTIVE &&
|
||||
event?.observation === "error"
|
||||
|
||||
21
frontend/src/hooks/query/use-app-installations.ts
Normal file
21
frontend/src/hooks/query/use-app-installations.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useConfig } from "./use-config";
|
||||
import { retrieveGitHubAppInstallations } from "#/api/github";
|
||||
|
||||
export const useAppInstallations = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { gitHubToken } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["installations", gitHubToken, config?.GITHUB_CLIENT_ID],
|
||||
queryFn: async () => {
|
||||
const data = await retrieveGitHubAppInstallations();
|
||||
return data;
|
||||
},
|
||||
enabled:
|
||||
!!gitHubToken &&
|
||||
!!config?.GITHUB_CLIENT_ID &&
|
||||
config?.APP_MODE === "saas",
|
||||
});
|
||||
};
|
||||
65
frontend/src/hooks/query/use-app-repositories.ts
Normal file
65
frontend/src/hooks/query/use-app-repositories.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveGitHubAppRepositories } from "#/api/github";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useAppInstallations } from "./use-app-installations";
|
||||
import { useConfig } from "./use-config";
|
||||
|
||||
export const useAppRepositories = () => {
|
||||
const { gitHubToken } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { data: installations } = useAppInstallations();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", gitHubToken, installations],
|
||||
queryFn: async ({
|
||||
pageParam,
|
||||
}: {
|
||||
pageParam: { installationIndex: number | null; repoPage: number | null };
|
||||
}) => {
|
||||
const { repoPage, installationIndex } = pageParam;
|
||||
|
||||
if (!installations) {
|
||||
throw new Error("Missing installation list");
|
||||
}
|
||||
|
||||
return retrieveGitHubAppRepositories(
|
||||
installationIndex || 0,
|
||||
installations,
|
||||
repoPage || 1,
|
||||
30,
|
||||
);
|
||||
},
|
||||
initialPageParam: { installationIndex: 0, repoPage: 1 },
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.nextPage) {
|
||||
return {
|
||||
installationIndex: lastPage.installationIndex,
|
||||
repoPage: lastPage.nextPage,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastPage.installationIndex !== null) {
|
||||
return { installationIndex: lastPage.installationIndex, repoPage: 1 };
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
enabled:
|
||||
!!gitHubToken &&
|
||||
Array.isArray(installations) &&
|
||||
installations.length > 0 &&
|
||||
config?.APP_MODE === "saas",
|
||||
});
|
||||
|
||||
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
|
||||
// (nextui autocomplete doesn't support onEndReached nor is it compatible for extending)
|
||||
const { isSuccess, isFetchingNextPage, hasNextPage, fetchNextPage } = repos;
|
||||
React.useEffect(() => {
|
||||
if (!isFetchingNextPage && isSuccess && hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [isFetchingNextPage, isSuccess, hasNextPage, fetchNextPage]);
|
||||
|
||||
return repos;
|
||||
};
|
||||
@@ -2,9 +2,11 @@ import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveGitHubUserRepositories } from "#/api/github";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useConfig } from "./use-config";
|
||||
|
||||
export const useUserRepositories = () => {
|
||||
const { gitHubToken } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", gitHubToken],
|
||||
@@ -12,7 +14,7 @@ export const useUserRepositories = () => {
|
||||
retrieveGitHubUserRepositories(pageParam, 100),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
enabled: !!gitHubToken,
|
||||
enabled: !!gitHubToken && config?.APP_MODE === "oss",
|
||||
});
|
||||
|
||||
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
|
||||
|
||||
@@ -11,15 +11,29 @@ import { useWsClient } from "#/context/ws-client-provider";
|
||||
The reason for this is that the hook exposes a ref that requires a DOM element to be rendered.
|
||||
*/
|
||||
|
||||
export const useTerminal = (
|
||||
commands: Command[] = [],
|
||||
secrets: string[] = [],
|
||||
) => {
|
||||
interface UseTerminalConfig {
|
||||
commands: Command[];
|
||||
secrets: string[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_TERMINAL_CONFIG: UseTerminalConfig = {
|
||||
commands: [],
|
||||
secrets: [],
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export const useTerminal = ({
|
||||
commands,
|
||||
secrets,
|
||||
disabled,
|
||||
}: UseTerminalConfig = DEFAULT_TERMINAL_CONFIG) => {
|
||||
const { send } = useWsClient();
|
||||
const terminal = React.useRef<Terminal | null>(null);
|
||||
const fitAddon = React.useRef<FitAddon | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const lastCommandIndex = React.useRef(0);
|
||||
const keyEventDisposable = React.useRef<{ dispose: () => void } | null>(null);
|
||||
|
||||
const createTerminal = () =>
|
||||
new Terminal({
|
||||
@@ -85,36 +99,12 @@ export const useTerminal = (
|
||||
terminal.current = createTerminal();
|
||||
fitAddon.current = new FitAddon();
|
||||
|
||||
let resizeObserver: ResizeObserver;
|
||||
let commandBuffer = "";
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
if (ref.current) {
|
||||
/* Initialize the terminal in the DOM */
|
||||
initializeTerminal();
|
||||
|
||||
terminal.current.write("$ ");
|
||||
terminal.current.onKey(({ key, domEvent }) => {
|
||||
if (domEvent.key === "Enter") {
|
||||
handleEnter(commandBuffer);
|
||||
commandBuffer = "";
|
||||
} else if (domEvent.key === "Backspace") {
|
||||
if (commandBuffer.length > 0) {
|
||||
commandBuffer = handleBackspace(commandBuffer);
|
||||
}
|
||||
} else {
|
||||
// Ignore paste event
|
||||
if (key.charCodeAt(0) === 22) {
|
||||
return;
|
||||
}
|
||||
commandBuffer += key;
|
||||
terminal.current?.write(key);
|
||||
}
|
||||
});
|
||||
terminal.current.attachCustomKeyEventHandler((event) =>
|
||||
pasteHandler(event, (text) => {
|
||||
commandBuffer += text;
|
||||
}),
|
||||
);
|
||||
|
||||
/* Listen for resize events */
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
@@ -125,7 +115,7 @@ export const useTerminal = (
|
||||
|
||||
return () => {
|
||||
terminal.current?.dispose();
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -152,5 +142,60 @@ export const useTerminal = (
|
||||
}
|
||||
}, [commands]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (terminal.current) {
|
||||
// Dispose of existing listeners if they exist
|
||||
if (keyEventDisposable.current) {
|
||||
keyEventDisposable.current.dispose();
|
||||
keyEventDisposable.current = null;
|
||||
}
|
||||
|
||||
let commandBuffer = "";
|
||||
|
||||
if (!disabled) {
|
||||
// Add new key event listener and store the disposable
|
||||
keyEventDisposable.current = terminal.current.onKey(
|
||||
({ key, domEvent }) => {
|
||||
if (domEvent.key === "Enter") {
|
||||
handleEnter(commandBuffer);
|
||||
commandBuffer = "";
|
||||
} else if (domEvent.key === "Backspace") {
|
||||
if (commandBuffer.length > 0) {
|
||||
commandBuffer = handleBackspace(commandBuffer);
|
||||
}
|
||||
} else {
|
||||
// Ignore paste event
|
||||
if (key.charCodeAt(0) === 22) {
|
||||
return;
|
||||
}
|
||||
commandBuffer += key;
|
||||
terminal.current?.write(key);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add custom key handler and store the disposable
|
||||
terminal.current.attachCustomKeyEventHandler((event) =>
|
||||
pasteHandler(event, (text) => {
|
||||
commandBuffer += text;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Add a noop handler when disabled
|
||||
keyEventDisposable.current = terminal.current.onKey((e) => {
|
||||
e.domEvent.preventDefault();
|
||||
e.domEvent.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (keyEventDisposable.current) {
|
||||
keyEventDisposable.current.dispose();
|
||||
keyEventDisposable.current = null;
|
||||
}
|
||||
};
|
||||
}, [disabled]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import posthog from "posthog-js";
|
||||
import { setImportedProjectZip } from "#/state/initial-query-slice";
|
||||
import { convertZipToBase64 } from "#/utils/convert-zip-to-base64";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
|
||||
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
@@ -25,7 +27,8 @@ function Home() {
|
||||
|
||||
const { data: config } = useConfig();
|
||||
const { data: user } = useGitHubUser();
|
||||
const { data: repositories } = useUserRepositories();
|
||||
const { data: appRepositories } = useAppRepositories();
|
||||
const { data: userRepositories } = useUserRepositories();
|
||||
|
||||
const gitHubAuthUrl = useGitHubAuthUrl({
|
||||
gitHubToken,
|
||||
@@ -52,7 +55,9 @@ function Home() {
|
||||
<GitHubRepositoriesSuggestionBox
|
||||
handleSubmit={() => formRef.current?.requestSubmit()}
|
||||
repositories={
|
||||
repositories?.pages.flatMap((page) => page.data) || []
|
||||
userRepositories?.pages.flatMap((page) => page.data) ||
|
||||
appRepositories?.pages.flatMap((page) => page.data) ||
|
||||
[]
|
||||
}
|
||||
gitHubAuthUrl={gitHubAuthUrl}
|
||||
user={user || null}
|
||||
|
||||
@@ -14,7 +14,7 @@ function Jupyter() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={parentRef}>
|
||||
<div ref={parentRef} className="h-full">
|
||||
<JupyterEditor maxWidth={parentWidth} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { isGitHubErrorReponse } from "#/api/github";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import {
|
||||
useWsClient,
|
||||
@@ -13,6 +12,7 @@ import { RootState } from "#/store";
|
||||
import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { useUploadFiles } from "../../../hooks/mutation/use-upload-files";
|
||||
import { useGitHubUser } from "../../../hooks/query/use-github-user";
|
||||
import { isGitHubErrorReponse } from "#/api/github-axios-instance";
|
||||
|
||||
export const useHandleRuntimeActive = () => {
|
||||
const { gitHubToken } = useAuth();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useConversationConfig } from "#/hooks/query/use-conversation-config";
|
||||
import { Container } from "#/components/layout/container";
|
||||
import Security from "#/components/shared/modals/security/security";
|
||||
import { CountBadge } from "#/components/layout/count-badge";
|
||||
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
|
||||
|
||||
function App() {
|
||||
const { token, gitHubToken } = useAuth();
|
||||
@@ -101,7 +102,10 @@ function App() {
|
||||
</Container>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<Container className="h-1/3 overflow-scroll" label="Terminal">
|
||||
<Container
|
||||
className="h-1/3 overflow-scroll"
|
||||
label={<TerminalStatusLabel />}
|
||||
>
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal secrets={secrets} />
|
||||
</React.Suspense>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useConfig } from "#/hooks/query/use-config";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import { WaitlistModal } from "#/components/features/waitlist/waitlist-modal";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
@@ -76,6 +77,9 @@ export default function MainApp() {
|
||||
const isInWaitlist =
|
||||
!isFetchingAuth && !isAuthed && config.data?.APP_MODE === "saas";
|
||||
|
||||
const { settingsAreUpToDate } = useUserPrefs();
|
||||
const [showAIConfig, setShowAIConfig] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="root-layout"
|
||||
@@ -96,6 +100,10 @@ export default function MainApp() {
|
||||
onClose={() => setConsentFormIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isAuthed || !settingsAreUpToDate) && showAIConfig && (
|
||||
<SettingsModal onClose={() => setShowAIConfig(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const LATEST_SETTINGS_VERSION = 3;
|
||||
export const LATEST_SETTINGS_VERSION = 4;
|
||||
|
||||
export type Settings = {
|
||||
LLM_MODEL: string;
|
||||
@@ -35,10 +35,11 @@ export const getCurrentSettingsVersion = () => {
|
||||
export const settingsAreUpToDate = () =>
|
||||
getCurrentSettingsVersion() === LATEST_SETTINGS_VERSION;
|
||||
|
||||
export const maybeMigrateSettings = () => {
|
||||
export const maybeMigrateSettings = (logout: () => void) => {
|
||||
// Sometimes we ship major changes, like a new default agent.
|
||||
// In this case, we may want to override a previous choice made by the user.
|
||||
const currentVersion = getCurrentSettingsVersion();
|
||||
|
||||
if (currentVersion < 1) {
|
||||
localStorage.setItem("AGENT", DEFAULT_SETTINGS.AGENT);
|
||||
}
|
||||
@@ -53,6 +54,10 @@ export const maybeMigrateSettings = () => {
|
||||
if (currentVersion < 3) {
|
||||
localStorage.removeItem("token");
|
||||
}
|
||||
|
||||
if (currentVersion < 4) {
|
||||
logout();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,8 +7,8 @@ export type Cell = {
|
||||
|
||||
const initialCells: Cell[] = [];
|
||||
|
||||
export const cellSlice = createSlice({
|
||||
name: "cell",
|
||||
export const jupyterSlice = createSlice({
|
||||
name: "jupyter",
|
||||
initialState: {
|
||||
cells: initialCells,
|
||||
},
|
||||
@@ -26,6 +26,7 @@ export const cellSlice = createSlice({
|
||||
});
|
||||
|
||||
export const { appendJupyterInput, appendJupyterOutput, clearJupyter } =
|
||||
cellSlice.actions;
|
||||
jupyterSlice.actions;
|
||||
|
||||
export default cellSlice.reducer;
|
||||
export const jupyterReducer = jupyterSlice.reducer;
|
||||
export default jupyterReducer;
|
||||
|
||||
@@ -6,7 +6,7 @@ import codeReducer from "./state/code-slice";
|
||||
import fileStateReducer from "./state/file-state-slice";
|
||||
import initialQueryReducer from "./state/initial-query-slice";
|
||||
import commandReducer from "./state/command-slice";
|
||||
import jupyterReducer from "./state/jupyter-slice";
|
||||
import { jupyterReducer } from "./state/jupyter-slice";
|
||||
import securityAnalyzerReducer from "./state/security-analyzer-slice";
|
||||
import statusReducer from "./state/status-slice";
|
||||
|
||||
|
||||
8
frontend/src/types/github.d.ts
vendored
8
frontend/src/types/github.d.ts
vendored
@@ -18,6 +18,10 @@ interface GitHubRepository {
|
||||
full_name: string;
|
||||
}
|
||||
|
||||
interface GitHubAppRepository {
|
||||
repositories: GitHubRepository[];
|
||||
}
|
||||
|
||||
interface GitHubCommit {
|
||||
html_url: string;
|
||||
sha: string;
|
||||
@@ -27,3 +31,7 @@ interface GitHubCommit {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GithubAppInstallation {
|
||||
installations: { id: number }[];
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
*/
|
||||
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
|
||||
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
|
||||
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=repo,user,workflow`;
|
||||
const scope = "repo,user,workflow,offline_access";
|
||||
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
|
||||
};
|
||||
|
||||
@@ -82,9 +82,9 @@ const saveSettingsView = (view: "basic" | "advanced") => {
|
||||
* Updates the settings version in local storage if the current settings are not up to date.
|
||||
* If the settings are outdated, it attempts to migrate them before updating the version.
|
||||
*/
|
||||
const updateSettingsVersion = () => {
|
||||
const updateSettingsVersion = (logout: () => void) => {
|
||||
if (!settingsAreUpToDate()) {
|
||||
maybeMigrateSettings();
|
||||
maybeMigrateSettings(logout);
|
||||
localStorage.setItem(
|
||||
"SETTINGS_VERSION",
|
||||
LATEST_SETTINGS_VERSION.toString(),
|
||||
|
||||
@@ -1,786 +0,0 @@
|
||||
import abc
|
||||
import difflib
|
||||
import logging
|
||||
import platform
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass
|
||||
from textwrap import dedent
|
||||
from typing import Literal, Union
|
||||
from warnings import warn
|
||||
|
||||
from browsergym.core.action.base import AbstractActionSet
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from browsergym.core.action.python import PythonActionSet
|
||||
|
||||
from openhands.agenthub.browsing_agent.utils import (
|
||||
ParseError,
|
||||
parse_html_tags_raise,
|
||||
)
|
||||
from openhands.runtime.browser.browser_env import BrowserEnv
|
||||
|
||||
|
||||
@dataclass
|
||||
class Flags:
|
||||
use_html: bool = True
|
||||
use_ax_tree: bool = False
|
||||
drop_ax_tree_first: bool = True # This flag is no longer active TODO delete
|
||||
use_thinking: bool = False
|
||||
use_error_logs: bool = False
|
||||
use_past_error_logs: bool = False
|
||||
use_history: bool = False
|
||||
use_action_history: bool = False
|
||||
use_memory: bool = False
|
||||
use_diff: bool = False
|
||||
html_type: str = 'pruned_html'
|
||||
use_concrete_example: bool = True
|
||||
use_abstract_example: bool = False
|
||||
multi_actions: bool = False
|
||||
action_space: Literal[
|
||||
'python', 'bid', 'coord', 'bid+coord', 'bid+nav', 'coord+nav', 'bid+coord+nav'
|
||||
] = 'bid'
|
||||
is_strict: bool = False
|
||||
# This flag will be automatically disabled `if not chat_model_args.has_vision()`
|
||||
use_screenshot: bool = True
|
||||
enable_chat: bool = False
|
||||
max_prompt_tokens: int = 100_000
|
||||
extract_visible_tag: bool = False
|
||||
extract_coords: Literal['False', 'center', 'box'] = 'False'
|
||||
extract_visible_elements_only: bool = False
|
||||
demo_mode: Literal['off', 'default', 'only_visible_elements'] = 'off'
|
||||
|
||||
def copy(self):
|
||||
return deepcopy(self)
|
||||
|
||||
def asdict(self):
|
||||
"""Helper for JSON serializble requirement."""
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(self, flags_dict):
|
||||
"""Helper for JSON serializable requirement."""
|
||||
if isinstance(flags_dict, Flags):
|
||||
return flags_dict
|
||||
|
||||
if not isinstance(flags_dict, dict):
|
||||
raise ValueError(
|
||||
f'Unregcognized type for flags_dict of type {type(flags_dict)}.'
|
||||
)
|
||||
return Flags(**flags_dict)
|
||||
|
||||
|
||||
class PromptElement:
|
||||
"""Base class for all prompt elements. Prompt elements can be hidden.
|
||||
|
||||
Prompt elements are used to build the prompt. Use flags to control which
|
||||
prompt elements are visible. We use class attributes as a convenient way
|
||||
to implement static prompts, but feel free to override them with instance
|
||||
attributes or @property decorator.
|
||||
"""
|
||||
|
||||
_prompt = ''
|
||||
_abstract_ex = ''
|
||||
_concrete_ex = ''
|
||||
|
||||
def __init__(self, visible: bool = True) -> None:
|
||||
"""Prompt element that can be hidden.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
visible : bool, optional
|
||||
Whether the prompt element should be visible, by default True. Can
|
||||
be a callable that returns a bool. This is useful when a specific
|
||||
flag changes during a shrink iteration.
|
||||
"""
|
||||
self._visible = visible
|
||||
|
||||
@property
|
||||
def prompt(self):
|
||||
"""Avoid overriding this method. Override _prompt instead."""
|
||||
return self._hide(self._prompt)
|
||||
|
||||
@property
|
||||
def abstract_ex(self):
|
||||
"""Useful when this prompt element is requesting an answer from the llm.
|
||||
Provide an abstract example of the answer here. See Memory for an
|
||||
example.
|
||||
|
||||
Avoid overriding this method. Override _abstract_ex instead
|
||||
"""
|
||||
return self._hide(self._abstract_ex)
|
||||
|
||||
@property
|
||||
def concrete_ex(self):
|
||||
"""Useful when this prompt element is requesting an answer from the llm.
|
||||
Provide a concrete example of the answer here. See Memory for an
|
||||
example.
|
||||
|
||||
Avoid overriding this method. Override _concrete_ex instead
|
||||
"""
|
||||
return self._hide(self._concrete_ex)
|
||||
|
||||
@property
|
||||
def is_visible(self):
|
||||
"""Handle the case where visible is a callable."""
|
||||
visible = self._visible
|
||||
if callable(visible):
|
||||
visible = visible()
|
||||
return visible
|
||||
|
||||
def _hide(self, value):
|
||||
"""Return value if visible is True, else return empty string."""
|
||||
if self.is_visible:
|
||||
return value
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _parse_answer(self, text_answer) -> dict:
|
||||
if self.is_visible:
|
||||
return self._parse_answer(text_answer)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class Shrinkable(PromptElement, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def shrink(self) -> None:
|
||||
"""Implement shrinking of this prompt element.
|
||||
|
||||
You need to recursively call all shrinkable elements that are part of
|
||||
this prompt. You can also implement a shrinking strategy for this prompt.
|
||||
Shrinking is can be called multiple times to progressively shrink the
|
||||
prompt until it fits max_tokens. Default max shrink iterations is 20.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Truncater(Shrinkable):
|
||||
"""A prompt element that can be truncated to fit the context length of the LLM.
|
||||
Of course, it will be great that we never have to use the functionality here to `shrink()` the prompt.
|
||||
Extend this class for prompt elements that can be truncated. Usually long observations such as AxTree or HTML.
|
||||
"""
|
||||
|
||||
def __init__(self, visible, shrink_speed=0.3, start_truncate_iteration=10):
|
||||
super().__init__(visible=visible)
|
||||
self.shrink_speed = shrink_speed # the percentage shrunk in each iteration
|
||||
self.start_truncate_iteration = (
|
||||
start_truncate_iteration # the iteration to start truncating
|
||||
)
|
||||
self.shrink_calls = 0
|
||||
self.deleted_lines = 0
|
||||
|
||||
def shrink(self) -> None:
|
||||
if self.is_visible and self.shrink_calls >= self.start_truncate_iteration:
|
||||
# remove the fraction of _prompt
|
||||
lines = self._prompt.splitlines()
|
||||
new_line_count = int(len(lines) * (1 - self.shrink_speed))
|
||||
self.deleted_lines += len(lines) - new_line_count
|
||||
self._prompt = '\n'.join(lines[:new_line_count])
|
||||
self._prompt += (
|
||||
f'\n... Deleted {self.deleted_lines} lines to reduce prompt size.'
|
||||
)
|
||||
|
||||
self.shrink_calls += 1
|
||||
|
||||
|
||||
def fit_tokens(
|
||||
shrinkable: Shrinkable,
|
||||
max_prompt_chars=None,
|
||||
max_iterations=20,
|
||||
):
|
||||
"""Shrink a prompt element until it fits max_tokens.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shrinkable : Shrinkable
|
||||
The prompt element to shrink.
|
||||
max_prompt_chars : int
|
||||
The maximum number of chars allowed.
|
||||
max_iterations : int, optional
|
||||
The maximum number of shrink iterations, by default 20.
|
||||
model_name : str, optional
|
||||
The name of the model used when tokenizing.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str : the prompt after shrinking.
|
||||
"""
|
||||
if max_prompt_chars is None:
|
||||
return shrinkable.prompt
|
||||
|
||||
for _ in range(max_iterations):
|
||||
prompt = shrinkable.prompt
|
||||
if isinstance(prompt, str):
|
||||
prompt_str = prompt
|
||||
elif isinstance(prompt, list):
|
||||
prompt_str = '\n'.join([p['text'] for p in prompt if p['type'] == 'text'])
|
||||
else:
|
||||
raise ValueError(f'Unrecognized type for prompt: {type(prompt)}')
|
||||
n_chars = len(prompt_str)
|
||||
if n_chars <= max_prompt_chars:
|
||||
return prompt
|
||||
shrinkable.shrink()
|
||||
|
||||
logging.info(
|
||||
dedent(
|
||||
f"""\
|
||||
After {max_iterations} shrink iterations, the prompt is still
|
||||
{len(prompt_str)} chars (greater than {max_prompt_chars}). Returning the prompt as is."""
|
||||
)
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
class HTML(Truncater):
|
||||
def __init__(self, html, visible: bool = True, prefix='') -> None:
|
||||
super().__init__(visible=visible, start_truncate_iteration=5)
|
||||
self._prompt = f'\n{prefix}HTML:\n{html}\n'
|
||||
|
||||
|
||||
class AXTree(Truncater):
|
||||
def __init__(
|
||||
self, ax_tree, visible: bool = True, coord_type=None, prefix=''
|
||||
) -> None:
|
||||
super().__init__(visible=visible, start_truncate_iteration=10)
|
||||
if coord_type == 'center':
|
||||
coord_note = """\
|
||||
Note: center coordinates are provided in parenthesis and are
|
||||
relative to the top left corner of the page.\n\n"""
|
||||
elif coord_type == 'box':
|
||||
coord_note = """\
|
||||
Note: bounding box of each object are provided in parenthesis and are
|
||||
relative to the top left corner of the page.\n\n"""
|
||||
else:
|
||||
coord_note = ''
|
||||
self._prompt = f'\n{prefix}AXTree:\n{coord_note}{ax_tree}\n'
|
||||
|
||||
|
||||
class Error(PromptElement):
|
||||
def __init__(self, error, visible: bool = True, prefix='') -> None:
|
||||
super().__init__(visible=visible)
|
||||
self._prompt = f'\n{prefix}Error from previous action:\n{error}\n'
|
||||
|
||||
|
||||
class Observation(Shrinkable):
|
||||
"""Observation of the current step.
|
||||
|
||||
Contains the html, the accessibility tree and the error logs.
|
||||
"""
|
||||
|
||||
def __init__(self, obs, flags: Flags) -> None:
|
||||
super().__init__()
|
||||
self.flags = flags
|
||||
self.obs = obs
|
||||
self.html = HTML(obs[flags.html_type], visible=flags.use_html, prefix='## ')
|
||||
self.ax_tree = AXTree(
|
||||
obs['axtree_txt'],
|
||||
visible=flags.use_ax_tree,
|
||||
coord_type=flags.extract_coords,
|
||||
prefix='## ',
|
||||
)
|
||||
self.error = Error(
|
||||
obs['last_action_error'],
|
||||
visible=flags.use_error_logs and obs['last_action_error'],
|
||||
prefix='## ',
|
||||
)
|
||||
|
||||
def shrink(self):
|
||||
self.ax_tree.shrink()
|
||||
self.html.shrink()
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
return f'\n# Observation of current step:\n{self.html.prompt}{self.ax_tree.prompt}{self.error.prompt}\n\n'
|
||||
|
||||
def add_screenshot(self, prompt):
|
||||
if self.flags.use_screenshot:
|
||||
if isinstance(prompt, str):
|
||||
prompt = [{'type': 'text', 'text': prompt}]
|
||||
img_url = BrowserEnv.image_to_jpg_base64_url(
|
||||
self.obs['screenshot'], add_data_prefix=True
|
||||
)
|
||||
prompt.append({'type': 'image_url', 'image_url': img_url})
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
class MacNote(PromptElement):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(visible=platform.system() == 'Darwin')
|
||||
self._prompt = '\nNote: you are on mac so you should use Meta instead of Control for Control+C etc.\n'
|
||||
|
||||
|
||||
class BeCautious(PromptElement):
|
||||
def __init__(self, visible: bool = True) -> None:
|
||||
super().__init__(visible=visible)
|
||||
self._prompt = """\
|
||||
\nBe very cautious. Avoid submitting anything before verifying the effect of your
|
||||
actions. Take the time to explore the effect of safe actions first. For example
|
||||
you can fill a few elements of a form, but don't click submit before verifying
|
||||
that everything was filled correctly.\n"""
|
||||
|
||||
|
||||
class GoalInstructions(PromptElement):
|
||||
def __init__(self, goal, visible: bool = True) -> None:
|
||||
super().__init__(visible)
|
||||
self._prompt = f"""\
|
||||
# Instructions
|
||||
Review the current state of the page and all other information to find the best
|
||||
possible next action to accomplish your goal. Your answer will be interpreted
|
||||
and executed by a program, make sure to follow the formatting instructions.
|
||||
|
||||
## Goal:
|
||||
{goal}
|
||||
"""
|
||||
|
||||
|
||||
class ChatInstructions(PromptElement):
|
||||
def __init__(self, chat_messages, visible: bool = True) -> None:
|
||||
super().__init__(visible)
|
||||
self._prompt = """\
|
||||
# Instructions
|
||||
|
||||
You are a UI Assistant, your goal is to help the user perform tasks using a web browser. You can
|
||||
communicate with the user via a chat, in which the user gives you instructions and in which you
|
||||
can send back messages. You have access to a web browser that both you and the user can see,
|
||||
and with which only you can interact via specific commands.
|
||||
|
||||
Review the instructions from the user, the current state of the page and all other information
|
||||
to find the best possible next action to accomplish your goal. Your answer will be interpreted
|
||||
and executed by a program, make sure to follow the formatting instructions.
|
||||
|
||||
## Chat messages:
|
||||
|
||||
"""
|
||||
self._prompt += '\n'.join(
|
||||
[
|
||||
f"""\
|
||||
- [{msg['role']}], {msg['message']}"""
|
||||
for msg in chat_messages
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SystemPrompt(PromptElement):
|
||||
_prompt = """\
|
||||
You are an agent trying to solve a web task based on the content of the page and
|
||||
a user instructions. You can interact with the page and explore. Each time you
|
||||
submit an action it will be sent to the browser and you will receive a new page."""
|
||||
|
||||
|
||||
class MainPrompt(Shrinkable):
|
||||
def __init__(
|
||||
self,
|
||||
obs_history,
|
||||
actions,
|
||||
memories,
|
||||
thoughts,
|
||||
flags: Flags,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.flags = flags
|
||||
self.history = History(obs_history, actions, memories, thoughts, flags)
|
||||
if self.flags.enable_chat:
|
||||
self.instructions: Union[ChatInstructions, GoalInstructions] = (
|
||||
ChatInstructions(obs_history[-1]['chat_messages'])
|
||||
)
|
||||
else:
|
||||
if (
|
||||
'chat_messages' in obs_history[-1]
|
||||
and sum(
|
||||
[msg['role'] == 'user' for msg in obs_history[-1]['chat_messages']]
|
||||
)
|
||||
> 1
|
||||
):
|
||||
logging.warning(
|
||||
'Agent is in goal mode, but multiple user messages are present in the chat. Consider switching to `enable_chat=True`.'
|
||||
)
|
||||
self.instructions = GoalInstructions(obs_history[-1]['goal'])
|
||||
|
||||
self.obs = Observation(obs_history[-1], self.flags)
|
||||
self.action_space = ActionSpace(self.flags)
|
||||
|
||||
self.think = Think(visible=flags.use_thinking)
|
||||
self.memory = Memory(visible=flags.use_memory)
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
prompt = f"""\
|
||||
{self.instructions.prompt}\
|
||||
{self.obs.prompt}\
|
||||
{self.history.prompt}\
|
||||
{self.action_space.prompt}\
|
||||
{self.think.prompt}\
|
||||
{self.memory.prompt}\
|
||||
"""
|
||||
|
||||
if self.flags.use_abstract_example:
|
||||
prompt += f"""
|
||||
# Abstract Example
|
||||
|
||||
Here is an abstract version of the answer with description of the content of
|
||||
each tag. Make sure you follow this structure, but replace the content with your
|
||||
answer:
|
||||
{self.think.abstract_ex}\
|
||||
{self.memory.abstract_ex}\
|
||||
{self.action_space.abstract_ex}\
|
||||
"""
|
||||
|
||||
if self.flags.use_concrete_example:
|
||||
prompt += f"""
|
||||
# Concrete Example
|
||||
|
||||
Here is a concrete example of how to format your answer.
|
||||
Make sure to follow the template with proper tags:
|
||||
{self.think.concrete_ex}\
|
||||
{self.memory.concrete_ex}\
|
||||
{self.action_space.concrete_ex}\
|
||||
"""
|
||||
return self.obs.add_screenshot(prompt)
|
||||
|
||||
def shrink(self):
|
||||
self.history.shrink()
|
||||
self.obs.shrink()
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
ans_dict = {}
|
||||
ans_dict.update(self.think._parse_answer(text_answer))
|
||||
ans_dict.update(self.memory._parse_answer(text_answer))
|
||||
ans_dict.update(self.action_space._parse_answer(text_answer))
|
||||
return ans_dict
|
||||
|
||||
|
||||
class ActionSpace(PromptElement):
|
||||
def __init__(self, flags: Flags) -> None:
|
||||
super().__init__()
|
||||
self.flags = flags
|
||||
self.action_space = _get_action_space(flags)
|
||||
|
||||
self._prompt = (
|
||||
f'# Action space:\n{self.action_space.describe()}{MacNote().prompt}\n'
|
||||
)
|
||||
self._abstract_ex = f"""
|
||||
<action>
|
||||
{self.action_space.example_action(abstract=True)}
|
||||
</action>
|
||||
"""
|
||||
self._concrete_ex = f"""
|
||||
<action>
|
||||
{self.action_space.example_action(abstract=False)}
|
||||
</action>
|
||||
"""
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
ans_dict = parse_html_tags_raise(
|
||||
text_answer, keys=['action'], merge_multiple=True
|
||||
)
|
||||
|
||||
try:
|
||||
# just check if action can be mapped to python code but keep action as is
|
||||
# the environment will be responsible for mapping it to python
|
||||
self.action_space.to_python_code(ans_dict['action'])
|
||||
except Exception as e:
|
||||
raise ParseError(
|
||||
f'Error while parsing action\n: {e}\n'
|
||||
'Make sure your answer is restricted to the allowed actions.'
|
||||
)
|
||||
|
||||
return ans_dict
|
||||
|
||||
|
||||
def _get_action_space(flags: Flags) -> AbstractActionSet:
|
||||
match flags.action_space:
|
||||
case 'python':
|
||||
action_space = PythonActionSet(strict=flags.is_strict)
|
||||
if flags.multi_actions:
|
||||
warn(
|
||||
f'Flag action_space={repr(flags.action_space)} incompatible with multi_actions={repr(flags.multi_actions)}.',
|
||||
stacklevel=2,
|
||||
)
|
||||
if flags.demo_mode != 'off':
|
||||
warn(
|
||||
f'Flag action_space={repr(flags.action_space)} incompatible with demo_mode={repr(flags.demo_mode)}.',
|
||||
stacklevel=2,
|
||||
)
|
||||
return action_space
|
||||
case 'bid':
|
||||
action_subsets = ['chat', 'bid']
|
||||
case 'coord':
|
||||
action_subsets = ['chat', 'coord']
|
||||
case 'bid+coord':
|
||||
action_subsets = ['chat', 'bid', 'coord']
|
||||
case 'bid+nav':
|
||||
action_subsets = ['chat', 'bid', 'nav']
|
||||
case 'coord+nav':
|
||||
action_subsets = ['chat', 'coord', 'nav']
|
||||
case 'bid+coord+nav':
|
||||
action_subsets = ['chat', 'bid', 'coord', 'nav']
|
||||
case _:
|
||||
raise NotImplementedError(
|
||||
f'Unknown action_space {repr(flags.action_space)}'
|
||||
)
|
||||
|
||||
action_space = HighLevelActionSet(
|
||||
subsets=action_subsets,
|
||||
multiaction=flags.multi_actions,
|
||||
strict=flags.is_strict,
|
||||
demo_mode=flags.demo_mode,
|
||||
)
|
||||
|
||||
return action_space
|
||||
|
||||
|
||||
class Memory(PromptElement):
|
||||
_prompt = '' # provided in the abstract and concrete examples
|
||||
|
||||
_abstract_ex = """
|
||||
<memory>
|
||||
Write down anything you need to remember for next steps. You will be presented
|
||||
with the list of previous memories and past actions.
|
||||
</memory>
|
||||
"""
|
||||
|
||||
_concrete_ex = """
|
||||
<memory>
|
||||
I clicked on bid 32 to activate tab 2. The accessibility tree should mention
|
||||
focusable for elements of the form at next step.
|
||||
</memory>
|
||||
"""
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
return parse_html_tags_raise(
|
||||
text_answer, optional_keys=['memory'], merge_multiple=True
|
||||
)
|
||||
|
||||
|
||||
class Think(PromptElement):
|
||||
_prompt = ''
|
||||
|
||||
_abstract_ex = """
|
||||
<think>
|
||||
Think step by step. If you need to make calculations such as coordinates, write them here. Describe the effect
|
||||
that your previous action had on the current content of the page.
|
||||
</think>
|
||||
"""
|
||||
_concrete_ex = """
|
||||
<think>
|
||||
My memory says that I filled the first name and last name, but I can't see any
|
||||
content in the form. I need to explore different ways to fill the form. Perhaps
|
||||
the form is not visible yet or some fields are disabled. I need to replan.
|
||||
</think>
|
||||
"""
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
return parse_html_tags_raise(
|
||||
text_answer, optional_keys=['think'], merge_multiple=True
|
||||
)
|
||||
|
||||
|
||||
def diff(previous, new):
|
||||
"""Return a string showing the difference between original and new.
|
||||
|
||||
If the difference is above diff_threshold, return the diff string.
|
||||
"""
|
||||
if previous == new:
|
||||
return 'Identical', []
|
||||
|
||||
if len(previous) == 0 or previous is None:
|
||||
return 'previous is empty', []
|
||||
|
||||
diff_gen = difflib.ndiff(previous.splitlines(), new.splitlines())
|
||||
|
||||
diff_lines = []
|
||||
plus_count = 0
|
||||
minus_count = 0
|
||||
for line in diff_gen:
|
||||
if line.strip().startswith('+'):
|
||||
diff_lines.append(line)
|
||||
plus_count += 1
|
||||
elif line.strip().startswith('-'):
|
||||
diff_lines.append(line)
|
||||
minus_count += 1
|
||||
else:
|
||||
continue
|
||||
|
||||
header = f'{plus_count} lines added and {minus_count} lines removed:'
|
||||
|
||||
return header, diff_lines
|
||||
|
||||
|
||||
class Diff(Shrinkable):
|
||||
def __init__(
|
||||
self, previous, new, prefix='', max_line_diff=20, shrink_speed=2, visible=True
|
||||
) -> None:
|
||||
super().__init__(visible=visible)
|
||||
self.max_line_diff = max_line_diff
|
||||
self.header, self.diff_lines = diff(previous, new)
|
||||
self.shrink_speed = shrink_speed
|
||||
self.prefix = prefix
|
||||
|
||||
def shrink(self):
|
||||
self.max_line_diff -= self.shrink_speed
|
||||
self.max_line_diff = max(1, self.max_line_diff)
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
diff_str = '\n'.join(self.diff_lines[: self.max_line_diff])
|
||||
if len(self.diff_lines) > self.max_line_diff:
|
||||
original_count = len(self.diff_lines)
|
||||
diff_str = f'{diff_str}\nDiff truncated, {original_count - self.max_line_diff} changes now shown.'
|
||||
return f'{self.prefix}{self.header}\n{diff_str}\n'
|
||||
|
||||
|
||||
class HistoryStep(Shrinkable):
|
||||
def __init__(
|
||||
self, previous_obs, current_obs, action, memory, flags: Flags, shrink_speed=1
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.html_diff = Diff(
|
||||
previous_obs[flags.html_type],
|
||||
current_obs[flags.html_type],
|
||||
prefix='\n### HTML diff:\n',
|
||||
shrink_speed=shrink_speed,
|
||||
visible=lambda: flags.use_html and flags.use_diff,
|
||||
)
|
||||
self.ax_tree_diff = Diff(
|
||||
previous_obs['axtree_txt'],
|
||||
current_obs['axtree_txt'],
|
||||
prefix='\n### Accessibility tree diff:\n',
|
||||
shrink_speed=shrink_speed,
|
||||
visible=lambda: flags.use_ax_tree and flags.use_diff,
|
||||
)
|
||||
self.error = Error(
|
||||
current_obs['last_action_error'],
|
||||
visible=(
|
||||
flags.use_error_logs
|
||||
and current_obs['last_action_error']
|
||||
and flags.use_past_error_logs
|
||||
),
|
||||
prefix='### ',
|
||||
)
|
||||
self.shrink_speed = shrink_speed
|
||||
self.action = action
|
||||
self.memory = memory
|
||||
self.flags = flags
|
||||
|
||||
def shrink(self):
|
||||
super().shrink()
|
||||
self.html_diff.shrink()
|
||||
self.ax_tree_diff.shrink()
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
prompt = ''
|
||||
|
||||
if self.flags.use_action_history:
|
||||
prompt += f'\n### Action:\n{self.action}\n'
|
||||
|
||||
prompt += (
|
||||
f'{self.error.prompt}{self.html_diff.prompt}{self.ax_tree_diff.prompt}'
|
||||
)
|
||||
|
||||
if self.flags.use_memory and self.memory is not None:
|
||||
prompt += f'\n### Memory:\n{self.memory}\n'
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
class History(Shrinkable):
|
||||
def __init__(
|
||||
self, history_obs, actions, memories, thoughts, flags: Flags, shrink_speed=1
|
||||
) -> None:
|
||||
super().__init__(visible=flags.use_history)
|
||||
assert len(history_obs) == len(actions) + 1
|
||||
assert len(history_obs) == len(memories) + 1
|
||||
|
||||
self.shrink_speed = shrink_speed
|
||||
self.history_steps: list[HistoryStep] = []
|
||||
|
||||
for i in range(1, len(history_obs)):
|
||||
self.history_steps.append(
|
||||
HistoryStep(
|
||||
history_obs[i - 1],
|
||||
history_obs[i],
|
||||
actions[i - 1],
|
||||
memories[i - 1],
|
||||
flags,
|
||||
)
|
||||
)
|
||||
|
||||
def shrink(self):
|
||||
"""Shrink individual steps"""
|
||||
# TODO set the shrink speed of older steps to be higher
|
||||
super().shrink()
|
||||
for step in self.history_steps:
|
||||
step.shrink()
|
||||
|
||||
@property
|
||||
def _prompt(self):
|
||||
prompts = ['# History of interaction with the task:\n']
|
||||
for i, step in enumerate(self.history_steps):
|
||||
prompts.append(f'## step {i}')
|
||||
prompts.append(step.prompt)
|
||||
return '\n'.join(prompts) + '\n'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
html_template = """
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
Hello World.
|
||||
Step {}.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
OBS_HISTORY = [
|
||||
{
|
||||
'goal': 'do this and that',
|
||||
'pruned_html': html_template.format(1),
|
||||
'axtree_txt': '[1] Click me',
|
||||
'last_action_error': '',
|
||||
},
|
||||
{
|
||||
'goal': 'do this and that',
|
||||
'pruned_html': html_template.format(2),
|
||||
'axtree_txt': '[1] Click me',
|
||||
'last_action_error': '',
|
||||
},
|
||||
{
|
||||
'goal': 'do this and that',
|
||||
'pruned_html': html_template.format(3),
|
||||
'axtree_txt': '[1] Click me',
|
||||
'last_action_error': 'Hey, there is an error now',
|
||||
},
|
||||
]
|
||||
ACTIONS = ["click('41')", "click('42')"]
|
||||
MEMORIES = ['memory A', 'memory B']
|
||||
THOUGHTS = ['thought A', 'thought B']
|
||||
|
||||
flags = Flags(
|
||||
use_html=True,
|
||||
use_ax_tree=True,
|
||||
use_thinking=True,
|
||||
use_error_logs=True,
|
||||
use_past_error_logs=True,
|
||||
use_history=True,
|
||||
use_action_history=True,
|
||||
use_memory=True,
|
||||
use_diff=True,
|
||||
html_type='pruned_html',
|
||||
use_concrete_example=True,
|
||||
use_abstract_example=True,
|
||||
use_screenshot=False,
|
||||
multi_actions=True,
|
||||
)
|
||||
|
||||
print(
|
||||
MainPrompt(
|
||||
obs_history=OBS_HISTORY,
|
||||
actions=ACTIONS,
|
||||
memories=MEMORIES,
|
||||
thoughts=THOUGHTS,
|
||||
flags=flags,
|
||||
).prompt
|
||||
)
|
||||
@@ -312,6 +312,20 @@ class AgentController:
|
||||
str(action),
|
||||
extra={'msg_type': 'ACTION', 'event_source': EventSource.USER},
|
||||
)
|
||||
# Extend max iterations when the user sends a message (only in non-headless mode)
|
||||
if self._initial_max_iterations is not None and not self.headless_mode:
|
||||
self.state.max_iterations = (
|
||||
self.state.iteration + self._initial_max_iterations
|
||||
)
|
||||
if (
|
||||
self.state.traffic_control_state == TrafficControlState.THROTTLING
|
||||
or self.state.traffic_control_state == TrafficControlState.PAUSED
|
||||
):
|
||||
self.state.traffic_control_state = TrafficControlState.NORMAL
|
||||
self.log(
|
||||
'debug',
|
||||
f'Extended max iterations to {self.state.max_iterations} after user message',
|
||||
)
|
||||
if self.get_agent_state() != AgentState.RUNNING:
|
||||
await self.set_agent_state_to(AgentState.RUNNING)
|
||||
elif action.source == EventSource.AGENT and action.wait_for_response:
|
||||
@@ -342,6 +356,7 @@ class AgentController:
|
||||
elif (
|
||||
new_state == AgentState.RUNNING
|
||||
and self.state.agent_state == AgentState.PAUSED
|
||||
# TODO: do we really need both THROTTLING and PAUSED states, or can we clean up one of them completely?
|
||||
and self.state.traffic_control_state == TrafficControlState.THROTTLING
|
||||
):
|
||||
# user intends to interrupt traffic control and let the task resume temporarily
|
||||
@@ -351,6 +366,7 @@ class AgentController:
|
||||
self.state.iteration is not None
|
||||
and self.state.max_iterations is not None
|
||||
and self._initial_max_iterations is not None
|
||||
and not self.headless_mode
|
||||
):
|
||||
if self.state.iteration >= self.state.max_iterations:
|
||||
self.state.max_iterations += self._initial_max_iterations
|
||||
@@ -631,16 +647,24 @@ class AgentController:
|
||||
self.state.traffic_control_state = TrafficControlState.NORMAL
|
||||
else:
|
||||
self.state.traffic_control_state = TrafficControlState.THROTTLING
|
||||
# Format values as integers for iterations, keep decimals for budget
|
||||
if limit_type == 'iteration':
|
||||
current_str = str(int(current_value))
|
||||
max_str = str(int(max_value))
|
||||
else:
|
||||
current_str = f'{current_value:.2f}'
|
||||
max_str = f'{max_value:.2f}'
|
||||
|
||||
if self.headless_mode:
|
||||
e = RuntimeError(
|
||||
f'Agent reached maximum {limit_type} in headless mode. '
|
||||
f'Current {limit_type}: {current_value:.2f}, max {limit_type}: {max_value:.2f}'
|
||||
f'Current {limit_type}: {current_str}, max {limit_type}: {max_str}'
|
||||
)
|
||||
await self._react_to_exception(e)
|
||||
else:
|
||||
e = RuntimeError(
|
||||
f'Agent reached maximum {limit_type}. '
|
||||
f'Current {limit_type}: {current_value:.2f}, max {limit_type}: {max_value:.2f}. '
|
||||
f'Current {limit_type}: {current_str}, max {limit_type}: {max_str}. '
|
||||
)
|
||||
# FIXME: this isn't really an exception--we should have a different path
|
||||
await self._react_to_exception(e)
|
||||
|
||||
@@ -66,9 +66,6 @@ class AppConfig:
|
||||
modal_api_token_secret: str = ''
|
||||
disable_color: bool = False
|
||||
jwt_secret: str = ''
|
||||
attach_session_middleware_class: str = (
|
||||
'openhands.server.middleware.AttachSessionMiddleware'
|
||||
)
|
||||
debug: bool = False
|
||||
file_uploads_max_file_size_mb: int = 0
|
||||
file_uploads_restrict_file_types: bool = False
|
||||
|
||||
@@ -38,6 +38,7 @@ class LLMConfig:
|
||||
output_cost_per_token: The cost per output token. This will available in logs for the user to check.
|
||||
ollama_base_url: The base URL for the OLLAMA API.
|
||||
drop_params: Drop any unmapped (unsupported) params without causing an exception.
|
||||
modify_params: Modify params allows litellm to do transformations like adding a default message, when a message is empty.
|
||||
disable_vision: If model is vision capable, this option allows to disable image processing (useful for cost reduction).
|
||||
caching_prompt: Use the prompt caching feature if provided by the LLM and supported by the provider.
|
||||
log_completions: Whether to log LLM completions to the state.
|
||||
@@ -72,7 +73,10 @@ class LLMConfig:
|
||||
input_cost_per_token: float | None = None
|
||||
output_cost_per_token: float | None = None
|
||||
ollama_base_url: str | None = None
|
||||
# This setting can be sent in each call to litellm
|
||||
drop_params: bool = True
|
||||
# Note: this setting is actually global, unlike drop_params
|
||||
modify_params: bool = True
|
||||
disable_vision: bool | None = None
|
||||
caching_prompt: bool = True
|
||||
log_completions: bool = False
|
||||
|
||||
@@ -101,7 +101,6 @@ class LLM(RetryMixin, DebugMixin):
|
||||
self.cost_metric_supported: bool = True
|
||||
self.config: LLMConfig = copy.deepcopy(config)
|
||||
|
||||
# litellm actually uses base Exception here for unknown model
|
||||
self.model_info: ModelInfo | None = None
|
||||
|
||||
if self.config.log_completions:
|
||||
@@ -206,6 +205,11 @@ class LLM(RetryMixin, DebugMixin):
|
||||
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||
}
|
||||
|
||||
# set litellm modify_params to the configured value
|
||||
# True by default to allow litellm to do transformations like adding a default message, when a message is empty
|
||||
# NOTE: this setting is global; unlike drop_params, it cannot be overridden in the litellm completion partial
|
||||
litellm.modify_params = self.config.modify_params
|
||||
|
||||
try:
|
||||
# Record start time for latency measurement
|
||||
start_time = time.time()
|
||||
|
||||
@@ -55,11 +55,9 @@ class Metrics:
|
||||
self._costs.append(Cost(cost=value, model=self.model_name))
|
||||
|
||||
def add_response_latency(self, value: float, response_id: str) -> None:
|
||||
if value < 0:
|
||||
raise ValueError('Response latency cannot be negative.')
|
||||
self._response_latencies.append(
|
||||
ResponseLatency(
|
||||
latency=value, model=self.model_name, response_id=response_id
|
||||
latency=max(0.0, value), model=self.model_name, response_id=response_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
action = CmdRunAction(
|
||||
command=f'git clone {url} {dir_name} ; cd {dir_name} ; git checkout -b openhands-workspace'
|
||||
)
|
||||
self.log('info', 'Cloning repo: {selected_repository}')
|
||||
self.log('info', f'Cloning repo: {selected_repository}')
|
||||
self.run_action(action)
|
||||
|
||||
def get_custom_microagents(self, selected_repository: str | None) -> list[str]:
|
||||
|
||||
@@ -43,6 +43,7 @@ from openhands.runtime.builder import DockerRuntimeBuilder
|
||||
from openhands.runtime.impl.eventstream.containers import remove_all_containers
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.runtime.utils.log_streamer import LogStreamer
|
||||
from openhands.runtime.utils.request import send_request
|
||||
from openhands.runtime.utils.runtime_build import build_runtime_image
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
@@ -58,68 +59,6 @@ def remove_all_runtime_containers():
|
||||
atexit.register(remove_all_runtime_containers)
|
||||
|
||||
|
||||
class LogBuffer:
|
||||
"""Synchronous buffer for Docker container logs.
|
||||
|
||||
This class provides a thread-safe way to collect, store, and retrieve logs
|
||||
from a Docker container. It uses a list to store log lines and provides methods
|
||||
for appending, retrieving, and clearing logs.
|
||||
"""
|
||||
|
||||
def __init__(self, container: docker.models.containers.Container, logFn: Callable):
|
||||
self.init_msg = 'Runtime client initialized.'
|
||||
|
||||
self.buffer: list[str] = []
|
||||
self.lock = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
self.log_generator = container.logs(stream=True, follow=True)
|
||||
self.log_stream_thread = threading.Thread(target=self.stream_logs)
|
||||
self.log_stream_thread.daemon = True
|
||||
self.log_stream_thread.start()
|
||||
self.log = logFn
|
||||
|
||||
def append(self, log_line: str):
|
||||
with self.lock:
|
||||
self.buffer.append(log_line)
|
||||
|
||||
def get_and_clear(self) -> list[str]:
|
||||
with self.lock:
|
||||
logs = list(self.buffer)
|
||||
self.buffer.clear()
|
||||
return logs
|
||||
|
||||
def stream_logs(self):
|
||||
"""Stream logs from the Docker container in a separate thread.
|
||||
|
||||
This method runs in its own thread to handle the blocking
|
||||
operation of reading log lines from the Docker SDK's synchronous generator.
|
||||
"""
|
||||
try:
|
||||
for log_line in self.log_generator:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
if log_line:
|
||||
decoded_line = log_line.decode('utf-8').rstrip()
|
||||
self.append(decoded_line)
|
||||
except Exception as e:
|
||||
self.log('error', f'Error streaming docker logs: {e}')
|
||||
|
||||
def __del__(self):
|
||||
if self.log_stream_thread.is_alive():
|
||||
self.log(
|
||||
'warn',
|
||||
"LogBuffer was not properly closed. Use 'log_buffer.close()' for clean shutdown.",
|
||||
)
|
||||
self.close(timeout=5)
|
||||
|
||||
def close(self, timeout: float = 5.0):
|
||||
self._stop_event.set()
|
||||
self.log_stream_thread.join(timeout)
|
||||
# Close the log generator to release the file descriptor
|
||||
if hasattr(self.log_generator, 'close'):
|
||||
self.log_generator.close()
|
||||
|
||||
|
||||
class EventStreamRuntime(Runtime):
|
||||
"""This runtime will subscribe the event stream.
|
||||
When receive an event, it will send the event to runtime-client which run inside the docker environment.
|
||||
@@ -186,7 +125,7 @@ class EventStreamRuntime(Runtime):
|
||||
self.runtime_builder = DockerRuntimeBuilder(self.docker_client)
|
||||
|
||||
# Buffer for container logs
|
||||
self.log_buffer: LogBuffer | None = None
|
||||
self.log_streamer: LogStreamer | None = None
|
||||
|
||||
self.init_base_runtime(
|
||||
config,
|
||||
@@ -241,7 +180,7 @@ class EventStreamRuntime(Runtime):
|
||||
f'Container started: {self.container_name}. VSCode URL: {self.vscode_url}',
|
||||
)
|
||||
|
||||
self.log_buffer = LogBuffer(self.container, self.log)
|
||||
self.log_streamer = LogStreamer(self.container, self.log)
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.log('info', f'Waiting for client to become ready at {self.api_url}...')
|
||||
@@ -407,27 +346,6 @@ class EventStreamRuntime(Runtime):
|
||||
f'attached to container: {self.container_name} {self._container_port} {self.api_url}',
|
||||
)
|
||||
|
||||
def _refresh_logs(self):
|
||||
self.log('debug', 'Getting container logs...')
|
||||
|
||||
assert (
|
||||
self.log_buffer is not None
|
||||
), 'Log buffer is expected to be initialized when container is started'
|
||||
|
||||
logs = self.log_buffer.get_and_clear()
|
||||
if logs:
|
||||
formatted_logs = '\n'.join([f' |{log}' for log in logs])
|
||||
self.log(
|
||||
'debug',
|
||||
'\n'
|
||||
+ '-' * 35
|
||||
+ 'Container logs:'
|
||||
+ '-' * 35
|
||||
+ f'\n{formatted_logs}'
|
||||
+ '\n'
|
||||
+ '-' * 80,
|
||||
)
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
retry=tenacity.retry_if_exception_type(
|
||||
@@ -446,8 +364,7 @@ class EventStreamRuntime(Runtime):
|
||||
except docker.errors.NotFound:
|
||||
raise RuntimeNotFoundError(f'Container {self.container_name} not found.')
|
||||
|
||||
self._refresh_logs()
|
||||
if not self.log_buffer:
|
||||
if not self.log_streamer:
|
||||
raise RuntimeError('Runtime client is not ready.')
|
||||
|
||||
with send_request(
|
||||
@@ -464,8 +381,8 @@ class EventStreamRuntime(Runtime):
|
||||
Parameters:
|
||||
- rm_all_containers (bool): Whether to remove all containers with the 'openhands-sandbox-' prefix
|
||||
"""
|
||||
if self.log_buffer:
|
||||
self.log_buffer.close()
|
||||
if self.log_streamer:
|
||||
self.log_streamer.close()
|
||||
|
||||
if self.session:
|
||||
self.session.close()
|
||||
@@ -513,8 +430,6 @@ class EventStreamRuntime(Runtime):
|
||||
'Action has been rejected by the user! Waiting for further user input.'
|
||||
)
|
||||
|
||||
self._refresh_logs()
|
||||
|
||||
assert action.timeout is not None
|
||||
|
||||
try:
|
||||
@@ -533,7 +448,7 @@ class EventStreamRuntime(Runtime):
|
||||
raise RuntimeError(
|
||||
f'Runtime failed to return execute_action before the requested timeout of {action.timeout}s'
|
||||
)
|
||||
self._refresh_logs()
|
||||
|
||||
return obs
|
||||
|
||||
def run(self, action: CmdRunAction) -> Observation:
|
||||
@@ -564,7 +479,6 @@ class EventStreamRuntime(Runtime):
|
||||
if not os.path.exists(host_src):
|
||||
raise FileNotFoundError(f'Source file {host_src} does not exist')
|
||||
|
||||
self._refresh_logs()
|
||||
try:
|
||||
if recursive:
|
||||
# For recursive copy, create a zip file
|
||||
@@ -609,14 +523,13 @@ class EventStreamRuntime(Runtime):
|
||||
self.log(
|
||||
'debug', f'Copy completed: host:{host_src} -> runtime:{sandbox_dest}'
|
||||
)
|
||||
self._refresh_logs()
|
||||
|
||||
def list_files(self, path: str | None = None) -> list[str]:
|
||||
"""List files in the sandbox.
|
||||
|
||||
If path is None, list files in the sandbox's initial working directory (e.g., /workspace).
|
||||
"""
|
||||
self._refresh_logs()
|
||||
|
||||
try:
|
||||
data = {}
|
||||
if path is not None:
|
||||
@@ -637,7 +550,7 @@ class EventStreamRuntime(Runtime):
|
||||
|
||||
def copy_from(self, path: str) -> Path:
|
||||
"""Zip all files in the sandbox and return as a stream of bytes."""
|
||||
self._refresh_logs()
|
||||
|
||||
try:
|
||||
params = {'path': path}
|
||||
with send_request(
|
||||
|
||||
@@ -12,7 +12,7 @@ from openhands.core.config import AppConfig
|
||||
from openhands.events import EventStream
|
||||
from openhands.runtime.impl.eventstream.eventstream_runtime import (
|
||||
EventStreamRuntime,
|
||||
LogBuffer,
|
||||
LogStreamer,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.utils.command import get_remote_startup_command
|
||||
@@ -32,24 +32,38 @@ def bytes_shim(string_generator) -> Generator[bytes, None, None]:
|
||||
yield line.encode('utf-8')
|
||||
|
||||
|
||||
class ModalLogBuffer(LogBuffer):
|
||||
"""Synchronous buffer for Modal sandbox logs.
|
||||
class ModalLogStreamer(LogStreamer):
|
||||
"""Streams Modal sandbox logs to stdout.
|
||||
|
||||
This class provides a thread-safe way to collect, store, and retrieve logs
|
||||
from a Modal sandbox. It uses a list to store log lines and provides methods
|
||||
for appending, retrieving, and clearing logs.
|
||||
This class provides a way to stream logs from a Modal sandbox directly to stdout
|
||||
through the provided logging function.
|
||||
"""
|
||||
|
||||
def __init__(self, sandbox: modal.Sandbox):
|
||||
self.init_msg = 'Runtime client initialized.'
|
||||
|
||||
self.buffer: list[str] = []
|
||||
self.lock = threading.Lock()
|
||||
def __init__(
|
||||
self,
|
||||
sandbox: modal.Sandbox,
|
||||
logFn: Callable,
|
||||
):
|
||||
self.log = logFn
|
||||
self._stop_event = threading.Event()
|
||||
self.log_generator = bytes_shim(sandbox.stderr)
|
||||
self.log_stream_thread = threading.Thread(target=self.stream_logs)
|
||||
self.log_stream_thread.daemon = True
|
||||
self.log_stream_thread.start()
|
||||
|
||||
# Start the stdout streaming thread
|
||||
self.stdout_thread = threading.Thread(target=self._stream_logs)
|
||||
self.stdout_thread.daemon = True
|
||||
self.stdout_thread.start()
|
||||
|
||||
def _stream_logs(self):
|
||||
"""Stream logs from the Modal sandbox."""
|
||||
try:
|
||||
for log_line in self.log_generator:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
if log_line:
|
||||
decoded_line = log_line.decode('utf-8').rstrip()
|
||||
self.log('debug', f'[inside sandbox] {decoded_line}')
|
||||
except Exception as e:
|
||||
self.log('error', f'Error streaming modal logs: {e}')
|
||||
|
||||
|
||||
class ModalRuntime(EventStreamRuntime):
|
||||
@@ -109,7 +123,7 @@ class ModalRuntime(EventStreamRuntime):
|
||||
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
|
||||
|
||||
# Buffer for container logs
|
||||
self.log_buffer: LogBuffer | None = None
|
||||
self.log_streamer: LogStreamer | None = None
|
||||
|
||||
if self.config.sandbox.runtime_extra_deps:
|
||||
self.log(
|
||||
@@ -156,7 +170,7 @@ class ModalRuntime(EventStreamRuntime):
|
||||
|
||||
self.send_status_message('STATUS$CONTAINER_STARTED')
|
||||
|
||||
self.log_buffer = ModalLogBuffer(self.sandbox)
|
||||
self.log_streamer = ModalLogStreamer(self.sandbox, self.log)
|
||||
if self.sandbox is None:
|
||||
raise Exception('Sandbox not initialized')
|
||||
tunnel = self.sandbox.tunnels()[self.container_port]
|
||||
@@ -278,11 +292,8 @@ echo 'export INPUTRC=/etc/inputrc' >> /etc/bash.bashrc
|
||||
|
||||
def close(self):
|
||||
"""Closes the ModalRuntime and associated objects."""
|
||||
# if self.temp_dir_handler:
|
||||
# self.temp_dir_handler.__exit__(None, None, None)
|
||||
|
||||
if self.log_buffer:
|
||||
self.log_buffer.close()
|
||||
if self.log_streamer:
|
||||
self.log_streamer.close()
|
||||
|
||||
if self.session:
|
||||
self.session.close()
|
||||
|
||||
@@ -149,7 +149,7 @@ class RemoteRuntime(Runtime):
|
||||
'GET',
|
||||
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
|
||||
is_retry=False,
|
||||
timeout=5,
|
||||
timeout=30,
|
||||
) as response:
|
||||
data = response.json()
|
||||
status = data.get('status')
|
||||
|
||||
@@ -12,49 +12,43 @@ from runloop_api_client.types.shared_params import LaunchParameters
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventStream
|
||||
from openhands.runtime.impl.eventstream.eventstream_runtime import (
|
||||
EventStreamRuntime,
|
||||
LogBuffer,
|
||||
)
|
||||
from openhands.runtime.impl.eventstream.eventstream_runtime import EventStreamRuntime
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.utils.command import get_remote_startup_command
|
||||
from openhands.runtime.utils.log_streamer import LogStreamer
|
||||
from openhands.runtime.utils.request import send_request
|
||||
from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
CONTAINER_NAME_PREFIX = 'openhands-runtime-'
|
||||
|
||||
|
||||
class RunloopLogBuffer(LogBuffer):
|
||||
"""Synchronous buffer for Runloop devbox logs.
|
||||
class RunloopLogStreamer(LogStreamer):
|
||||
"""Streams Runloop devbox logs to stdout.
|
||||
|
||||
This class provides a thread-safe way to collect, store, and retrieve logs
|
||||
from a Docker container. It uses a list to store log lines and provides methods
|
||||
for appending, retrieving, and clearing logs.
|
||||
This class provides a way to stream logs from a Runloop devbox directly to stdout
|
||||
through the provided logging function.
|
||||
"""
|
||||
|
||||
def __init__(self, runloop_api_client: Runloop, devbox_id: str):
|
||||
self.client_ready = False
|
||||
self.init_msg = 'Runtime client initialized.'
|
||||
|
||||
self.buffer: list[str] = []
|
||||
self.lock = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
def __init__(
|
||||
self,
|
||||
runloop_api_client: Runloop,
|
||||
devbox_id: str,
|
||||
logFn: Callable,
|
||||
):
|
||||
self.runloop_api_client = runloop_api_client
|
||||
self.devbox_id = devbox_id
|
||||
self.log = logFn
|
||||
self.log_index = 0
|
||||
self.log_stream_thread = threading.Thread(target=self.stream_logs)
|
||||
self.log_stream_thread.daemon = True
|
||||
self.log_stream_thread.start()
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def stream_logs(self):
|
||||
"""Stream logs from the Docker container in a separate thread.
|
||||
|
||||
This method runs in its own thread to handle the blocking
|
||||
operation of reading log lines from the Docker SDK's synchronous generator.
|
||||
"""
|
||||
# Start the stdout streaming thread
|
||||
self.stdout_thread = threading.Thread(target=self._stream_logs)
|
||||
self.stdout_thread.daemon = True
|
||||
self.stdout_thread.start()
|
||||
|
||||
def _stream_logs(self):
|
||||
"""Stream logs from the Runloop devbox."""
|
||||
try:
|
||||
# TODO(Runloop) Replace with stream
|
||||
while True:
|
||||
raw_logs = self.runloop_api_client.devboxes.logs.list(
|
||||
self.devbox_id
|
||||
@@ -70,29 +64,11 @@ class RunloopLogBuffer(LogBuffer):
|
||||
break
|
||||
if logs:
|
||||
for log_line in logs:
|
||||
self.append(log_line)
|
||||
if self.init_msg in log_line:
|
||||
self.client_ready = True
|
||||
self.log('debug', f'[inside devbox] {log_line}')
|
||||
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f'Error streaming runloop logs: {e}')
|
||||
|
||||
# NB: Match LogBuffer behavior on below methods
|
||||
|
||||
def get_and_clear(self) -> list[str]:
|
||||
with self.lock:
|
||||
logs = list(self.buffer)
|
||||
self.buffer.clear()
|
||||
return logs
|
||||
|
||||
def append(self, log_line: str):
|
||||
with self.lock:
|
||||
self.buffer.append(log_line)
|
||||
|
||||
def close(self, timeout: float = 5.0):
|
||||
self._stop_event.set()
|
||||
self.log_stream_thread.join(timeout)
|
||||
self.log('error', f'Error streaming runloop logs: {e}')
|
||||
|
||||
|
||||
class RunloopRuntime(EventStreamRuntime):
|
||||
@@ -132,7 +108,7 @@ class RunloopRuntime(EventStreamRuntime):
|
||||
headless_mode,
|
||||
)
|
||||
# Buffer for container logs
|
||||
self.log_buffer: LogBuffer | None = None
|
||||
self.log_streamer: LogStreamer | None = None
|
||||
self._vscode_url: str | None = None
|
||||
|
||||
@tenacity.retry(
|
||||
@@ -224,7 +200,9 @@ class RunloopRuntime(EventStreamRuntime):
|
||||
)
|
||||
|
||||
# Hook up logs
|
||||
self.log_buffer = RunloopLogBuffer(self.runloop_api_client, self.devbox.id)
|
||||
self.log_streamer = RunloopLogStreamer(
|
||||
self.runloop_api_client, self.devbox.id, logger.info
|
||||
)
|
||||
self.api_url = tunnel.url
|
||||
logger.info(f'Container started. Server url: {self.api_url}')
|
||||
|
||||
@@ -248,9 +226,7 @@ class RunloopRuntime(EventStreamRuntime):
|
||||
reraise=(ConnectionRefusedError,),
|
||||
)
|
||||
def _wait_until_alive(self):
|
||||
# NB(Runloop): Remote logs are not guaranteed realtime, removing client_ready check from logs
|
||||
self._refresh_logs()
|
||||
if not self.log_buffer:
|
||||
if not self.log_streamer:
|
||||
raise RuntimeError('Runtime client is not ready.')
|
||||
response = send_request(
|
||||
self.session,
|
||||
@@ -266,8 +242,8 @@ class RunloopRuntime(EventStreamRuntime):
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def close(self, rm_all_containers: bool | None = True):
|
||||
if self.log_buffer:
|
||||
self.log_buffer.close()
|
||||
if self.log_streamer:
|
||||
self.log_streamer.close()
|
||||
|
||||
if self.session:
|
||||
self.session.close()
|
||||
|
||||
51
openhands/runtime/utils/log_streamer.py
Normal file
51
openhands/runtime/utils/log_streamer.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
import docker
|
||||
|
||||
|
||||
class LogStreamer:
|
||||
"""Streams Docker container logs to stdout.
|
||||
|
||||
This class provides a way to stream logs from a Docker container directly to stdout
|
||||
through the provided logging function.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
container: docker.models.containers.Container,
|
||||
logFn: Callable,
|
||||
):
|
||||
self.log = logFn
|
||||
self.log_generator = container.logs(stream=True, follow=True)
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
# Start the stdout streaming thread
|
||||
self.stdout_thread = threading.Thread(target=self._stream_logs)
|
||||
self.stdout_thread.daemon = True
|
||||
self.stdout_thread.start()
|
||||
|
||||
def _stream_logs(self):
|
||||
"""Stream logs from the Docker container to stdout."""
|
||||
try:
|
||||
for log_line in self.log_generator:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
if log_line:
|
||||
decoded_line = log_line.decode('utf-8').rstrip()
|
||||
self.log('debug', f'[inside container] {decoded_line}')
|
||||
except Exception as e:
|
||||
self.log('error', f'Error streaming docker logs to stdout: {e}')
|
||||
|
||||
def __del__(self):
|
||||
if self.stdout_thread and self.stdout_thread.is_alive():
|
||||
self.close(timeout=5)
|
||||
|
||||
def close(self, timeout: float = 5.0):
|
||||
"""Clean shutdown of the log streaming."""
|
||||
self._stop_event.set()
|
||||
if self.stdout_thread and self.stdout_thread.is_alive():
|
||||
self.stdout_thread.join(timeout)
|
||||
# Close the log generator to release the file descriptor
|
||||
if hasattr(self.log_generator, 'close'):
|
||||
self.log_generator.close()
|
||||
@@ -16,13 +16,13 @@ from openhands.server.middleware import (
|
||||
NoCacheMiddleware,
|
||||
RateLimitMiddleware,
|
||||
)
|
||||
from openhands.server.routes.auth import app as auth_api_router
|
||||
from openhands.server.routes.conversation import app as conversation_api_router
|
||||
from openhands.server.routes.feedback import app as feedback_api_router
|
||||
from openhands.server.routes.files import app as files_api_router
|
||||
from openhands.server.routes.github import app as github_api_router
|
||||
from openhands.server.routes.public import app as public_api_router
|
||||
from openhands.server.routes.security import app as security_api_router
|
||||
from openhands.server.shared import config, session_manager
|
||||
from openhands.server.shared import openhands_config, session_manager
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
@@ -51,15 +51,16 @@ async def health():
|
||||
return 'OK'
|
||||
|
||||
|
||||
app.include_router(auth_api_router)
|
||||
app.include_router(public_api_router)
|
||||
app.include_router(files_api_router)
|
||||
app.include_router(conversation_api_router)
|
||||
app.include_router(security_api_router)
|
||||
app.include_router(feedback_api_router)
|
||||
app.include_router(github_api_router)
|
||||
|
||||
|
||||
AttachSessionMiddlewareImpl = get_impl(
|
||||
AttachSessionMiddleware, config.attach_session_middleware_class
|
||||
AttachSessionMiddleware, openhands_config.attach_session_middleware_path
|
||||
)
|
||||
app.middleware('http')(AttachSessionMiddlewareImpl(app, target_router=files_api_router))
|
||||
app.middleware('http')(
|
||||
|
||||
@@ -30,10 +30,10 @@ def get_sid_from_token(token: str, jwt_secret: str) -> str:
|
||||
return ''
|
||||
|
||||
|
||||
def sign_token(payload: dict[str, object], jwt_secret: str) -> str:
|
||||
def sign_token(payload: dict[str, object], jwt_secret: str, algorithm='HS256') -> str:
|
||||
"""Signs a JWT token."""
|
||||
# payload = {
|
||||
# "sid": sid,
|
||||
# # "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
|
||||
# }
|
||||
return jwt.encode(payload, jwt_secret, algorithm='HS256')
|
||||
return jwt.encode(payload, jwt_secret, algorithm=algorithm)
|
||||
|
||||
58
openhands/server/config/openhands_config.py
Normal file
58
openhands/server/config/openhands_config.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.types import AppMode, OpenhandsConfigInterface
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
class OpenhandsConfig(OpenhandsConfigInterface):
|
||||
config_cls = os.environ.get('OPENHANDS_CONFIG_CLS', None)
|
||||
app_mode = AppMode.OSS
|
||||
posthog_client_key = 'phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA'
|
||||
github_client_id = os.environ.get('GITHUB_APP_CLIENT_ID', '')
|
||||
attach_session_middleware_path = (
|
||||
'openhands.server.middleware.AttachSessionMiddleware'
|
||||
)
|
||||
|
||||
def verify_config(self):
|
||||
if self.config_cls:
|
||||
raise ValueError('Unexpected config path provided')
|
||||
|
||||
def verify_github_repo_list(self, installation_id: int | None):
|
||||
if self.app_mode == AppMode.OSS and installation_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail='Unexpected installation ID',
|
||||
)
|
||||
|
||||
def get_config(self):
|
||||
config = {
|
||||
'APP_MODE': self.app_mode,
|
||||
'GITHUB_CLIENT_ID': self.github_client_id,
|
||||
'POSTHOG_CLIENT_KEY': self.posthog_client_key,
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
async def github_auth(self, data: dict):
|
||||
"""
|
||||
Skip Github Auth for AppMode OSS
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def load_openhands_config():
|
||||
config_cls = os.environ.get('OPENHANDS_CONFIG_CLS', None)
|
||||
logger.info(f'Using config class {config_cls}')
|
||||
|
||||
if config_cls:
|
||||
openhands_config_cls = get_impl(OpenhandsConfig, config_cls)
|
||||
else:
|
||||
openhands_config_cls = OpenhandsConfig
|
||||
|
||||
openhands_config = openhands_config_cls()
|
||||
openhands_config.verify_config()
|
||||
|
||||
return openhands_config
|
||||
@@ -1,129 +0,0 @@
|
||||
import os
|
||||
|
||||
from github import Github
|
||||
from github.GithubException import GithubException
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.sheets_client import GoogleSheetsClient
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID', '').strip()
|
||||
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET', '').strip()
|
||||
|
||||
|
||||
class UserVerifier:
|
||||
def __init__(self) -> None:
|
||||
logger.debug('Initializing UserVerifier')
|
||||
self.file_users: list[str] | None = None
|
||||
self.sheets_client: GoogleSheetsClient | None = None
|
||||
self.spreadsheet_id: str | None = None
|
||||
|
||||
# Initialize from environment variables
|
||||
self._init_file_users()
|
||||
self._init_sheets_client()
|
||||
|
||||
def _init_file_users(self) -> None:
|
||||
"""Load users from text file if configured"""
|
||||
waitlist = os.getenv('GITHUB_USER_LIST_FILE')
|
||||
if not waitlist:
|
||||
logger.debug('GITHUB_USER_LIST_FILE not configured')
|
||||
return
|
||||
|
||||
if not os.path.exists(waitlist):
|
||||
logger.error(f'User list file not found: {waitlist}')
|
||||
raise FileNotFoundError(f'User list file not found: {waitlist}')
|
||||
|
||||
try:
|
||||
with open(waitlist, 'r') as f:
|
||||
self.file_users = [line.strip() for line in f if line.strip()]
|
||||
logger.info(
|
||||
f'Successfully loaded {len(self.file_users)} users from {waitlist}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error reading user list file {waitlist}: {str(e)}')
|
||||
|
||||
def _init_sheets_client(self) -> None:
|
||||
"""Initialize Google Sheets client if configured"""
|
||||
sheet_id = os.getenv('GITHUB_USERS_SHEET_ID')
|
||||
|
||||
if not sheet_id:
|
||||
logger.debug('GITHUB_USERS_SHEET_ID not configured')
|
||||
return
|
||||
|
||||
logger.debug('Initializing Google Sheets integration')
|
||||
self.sheets_client = GoogleSheetsClient()
|
||||
self.spreadsheet_id = sheet_id
|
||||
|
||||
def is_active(self) -> bool:
|
||||
return bool(self.file_users or (self.sheets_client and self.spreadsheet_id))
|
||||
|
||||
def is_user_allowed(self, username: str) -> bool:
|
||||
"""Check if user is allowed based on file and/or sheet configuration"""
|
||||
if not self.is_active():
|
||||
return True
|
||||
|
||||
logger.debug(f'Checking if GitHub user {username} is allowed')
|
||||
if self.file_users:
|
||||
if username in self.file_users:
|
||||
logger.debug(f'User {username} found in text file allowlist')
|
||||
return True
|
||||
logger.debug(f'User {username} not found in text file allowlist')
|
||||
|
||||
if self.sheets_client and self.spreadsheet_id:
|
||||
sheet_users = self.sheets_client.get_usernames(self.spreadsheet_id)
|
||||
if username in sheet_users:
|
||||
logger.debug(f'User {username} found in Google Sheets allowlist')
|
||||
return True
|
||||
logger.debug(f'User {username} not found in Google Sheets allowlist')
|
||||
|
||||
logger.debug(f'User {username} not found in any allowlist')
|
||||
return False
|
||||
|
||||
|
||||
async def authenticate_github_user(auth_token) -> bool:
|
||||
user_verifier = UserVerifier()
|
||||
|
||||
if not user_verifier.is_active():
|
||||
logger.debug('No user verification sources configured - allowing all users')
|
||||
return True
|
||||
|
||||
logger.debug('Checking GitHub token')
|
||||
|
||||
if not auth_token:
|
||||
logger.warning('No GitHub token provided')
|
||||
return False
|
||||
|
||||
login = await get_github_user(auth_token)
|
||||
|
||||
if not user_verifier.is_user_allowed(login):
|
||||
logger.warning(f'GitHub user {login} not in allow list')
|
||||
return False
|
||||
|
||||
logger.info(f'GitHub user {login} authenticated')
|
||||
return True
|
||||
|
||||
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=5))
|
||||
async def get_github_user(token: str) -> str:
|
||||
"""Get GitHub user info from token.
|
||||
|
||||
Args:
|
||||
token: GitHub access token
|
||||
|
||||
Returns:
|
||||
github handle of the user
|
||||
"""
|
||||
logger.debug('Fetching GitHub user info from token')
|
||||
g = Github(token)
|
||||
try:
|
||||
user = await call_sync_from_async(g.get_user)
|
||||
except GithubException as e:
|
||||
logger.error(f'Error making request to GitHub API: {str(e)}')
|
||||
logger.error(e)
|
||||
raise
|
||||
finally:
|
||||
g.close()
|
||||
login = user.login
|
||||
logger.info(f'Successfully retrieved GitHub user: {login}')
|
||||
return login
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi import status
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema.action import ActionType
|
||||
from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import (
|
||||
NullAction,
|
||||
)
|
||||
@@ -12,9 +11,8 @@ from openhands.events.observation.agent import AgentStateChangedObservation
|
||||
from openhands.events.serialization import event_to_dict
|
||||
from openhands.events.stream import AsyncEventStreamWrapper
|
||||
from openhands.server.auth import get_sid_from_token, sign_token
|
||||
from openhands.server.github_utils import authenticate_github_user
|
||||
from openhands.server.session.session_init_data import SessionInitData
|
||||
from openhands.server.shared import config, session_manager, sio
|
||||
from openhands.server.shared import config, openhands_config, session_manager, sio
|
||||
|
||||
|
||||
@sio.event
|
||||
@@ -27,16 +25,15 @@ async def oh_action(connection_id: str, data: dict):
|
||||
# If it's an init, we do it here.
|
||||
action = data.get('action', '')
|
||||
if action == ActionType.INIT:
|
||||
token = data.pop('token', None)
|
||||
await openhands_config.github_auth(data)
|
||||
github_token = data.pop('github_token', None)
|
||||
token = data.pop('token', None)
|
||||
latest_event_id = int(data.pop('latest_event_id', -1))
|
||||
kwargs = {k.lower(): v for k, v in (data.get('args') or {}).items()}
|
||||
session_init_data = SessionInitData(**kwargs)
|
||||
session_init_data.github_token = github_token
|
||||
session_init_data.selected_repository = data.get('selected_repository', None)
|
||||
await init_connection(
|
||||
connection_id, token, github_token, session_init_data, latest_event_id
|
||||
)
|
||||
await init_connection(connection_id, token, session_init_data, latest_event_id)
|
||||
return
|
||||
|
||||
logger.info(f'sio:oh_action:{connection_id}')
|
||||
@@ -46,13 +43,9 @@ async def oh_action(connection_id: str, data: dict):
|
||||
async def init_connection(
|
||||
connection_id: str,
|
||||
token: str | None,
|
||||
gh_token: str | None,
|
||||
session_init_data: SessionInitData,
|
||||
latest_event_id: int,
|
||||
):
|
||||
if not await authenticate_github_user(gh_token):
|
||||
raise RuntimeError(status.WS_1008_POLICY_VIOLATION)
|
||||
|
||||
if token:
|
||||
sid = get_sid_from_token(token, config.jwt_secret)
|
||||
if sid == '':
|
||||
@@ -84,8 +77,9 @@ async def init_connection(
|
||||
):
|
||||
continue
|
||||
elif isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state == AgentState.INIT:
|
||||
await sio.emit('oh_event', event_to_dict(event), to=connection_id)
|
||||
agent_state_changed = event
|
||||
continue
|
||||
await sio.emit('oh_event', event_to_dict(event), to=connection_id)
|
||||
if agent_state_changed:
|
||||
await sio.emit('oh_event', event_to_dict(agent_state_changed), to=connection_id)
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import datetime, timedelta
|
||||
from typing import Callable
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, Request, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -13,8 +12,8 @@ from starlette.types import ASGIApp
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.auth import get_sid_from_token
|
||||
from openhands.server.github_utils import UserVerifier
|
||||
from openhands.server.shared import config, session_manager
|
||||
from openhands.server.types import SessionMiddlewareInterface
|
||||
|
||||
|
||||
class LocalhostCORSMiddleware(CORSMiddleware):
|
||||
@@ -109,53 +108,32 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
class AttachSessionMiddleware:
|
||||
class AttachSessionMiddleware(SessionMiddlewareInterface):
|
||||
def __init__(self, app, target_router: APIRouter):
|
||||
self.app = app
|
||||
self.target_router = target_router
|
||||
self.target_paths = {route.path for route in target_router.routes}
|
||||
|
||||
async def __call__(self, request: Request, call_next: Callable):
|
||||
do_attach = False
|
||||
if request.url.path in self.target_paths:
|
||||
do_attach = True
|
||||
|
||||
def _should_attach(self, request) -> bool:
|
||||
"""
|
||||
Determine if the middleware should attach a session for the given request.
|
||||
"""
|
||||
if request.method == 'OPTIONS':
|
||||
do_attach = False
|
||||
return False
|
||||
if request.url.path not in self.target_paths:
|
||||
return False
|
||||
return True
|
||||
|
||||
if not do_attach:
|
||||
return await call_next(request)
|
||||
|
||||
user_verifier = UserVerifier()
|
||||
if user_verifier.is_active():
|
||||
signed_token = request.cookies.get('github_auth')
|
||||
if not signed_token:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={'error': 'Not authenticated'},
|
||||
)
|
||||
try:
|
||||
jwt.decode(signed_token, config.jwt_secret, algorithms=['HS256'])
|
||||
except Exception as e:
|
||||
logger.warning(f'Invalid token: {e}')
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={'error': 'Invalid token'},
|
||||
)
|
||||
|
||||
if not request.headers.get('Authorization'):
|
||||
logger.warning('Missing Authorization header')
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={'error': 'Missing Authorization header'},
|
||||
)
|
||||
|
||||
auth_token = request.headers.get('Authorization')
|
||||
async def _attach_session(self, request: Request) -> JSONResponse | None:
|
||||
"""
|
||||
Attach the user's session based on the provided authentication token.
|
||||
"""
|
||||
auth_token = request.headers.get('Authorization', '')
|
||||
if 'Bearer' in auth_token:
|
||||
auth_token = auth_token.split('Bearer')[1].strip()
|
||||
|
||||
request.state.sid = get_sid_from_token(auth_token, config.jwt_secret)
|
||||
if request.state.sid == '':
|
||||
if not request.state.sid:
|
||||
logger.warning('Invalid token')
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -165,13 +143,32 @@ class AttachSessionMiddleware:
|
||||
request.state.conversation = await session_manager.attach_to_conversation(
|
||||
request.state.sid
|
||||
)
|
||||
if request.state.conversation is None:
|
||||
if not request.state.conversation:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={'error': 'Session not found'},
|
||||
)
|
||||
return None
|
||||
|
||||
async def _detach_session(self, request: Request) -> None:
|
||||
"""
|
||||
Detach the user's session.
|
||||
"""
|
||||
await session_manager.detach_from_conversation(request.state.conversation)
|
||||
|
||||
async def __call__(self, request: Request, call_next: Callable):
|
||||
if not self._should_attach(request):
|
||||
return await call_next(request)
|
||||
|
||||
response = await self._attach_session(request)
|
||||
if response:
|
||||
return response
|
||||
|
||||
try:
|
||||
# Continue processing the request
|
||||
response = await call_next(request)
|
||||
finally:
|
||||
await session_manager.detach_from_conversation(request.state.conversation)
|
||||
# Ensure the session is detached
|
||||
await self._detach_session(request)
|
||||
|
||||
return response
|
||||
|
||||
@@ -58,5 +58,15 @@ def refresh_files():
|
||||
return ['hello_world.py']
|
||||
|
||||
|
||||
@app.get('/api/options/config')
|
||||
def get_config():
|
||||
return {'APP_MODE': 'oss'}
|
||||
|
||||
|
||||
@app.get('/api/options/security-analyzers')
|
||||
def get_analyzers():
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(app, host='127.0.0.1', port=3000)
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import time
|
||||
import warnings
|
||||
|
||||
import requests
|
||||
|
||||
from openhands.server.github_utils import (
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
authenticate_github_user,
|
||||
)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Request,
|
||||
status,
|
||||
)
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.auth import sign_token
|
||||
from openhands.server.shared import config
|
||||
|
||||
app = APIRouter(prefix='/api')
|
||||
|
||||
|
||||
class AuthCode(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
@app.post('/github/callback')
|
||||
def github_callback(auth_code: AuthCode):
|
||||
# Prepare data for the token exchange request
|
||||
data = {
|
||||
'client_id': GITHUB_CLIENT_ID,
|
||||
'client_secret': GITHUB_CLIENT_SECRET,
|
||||
'code': auth_code.code,
|
||||
}
|
||||
|
||||
logger.debug('Exchanging code for GitHub token')
|
||||
|
||||
headers = {'Accept': 'application/json'}
|
||||
response = requests.post(
|
||||
'https://github.com/login/oauth/access_token', data=data, headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f'Failed to exchange code for token: {response.text}')
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={'error': 'Failed to exchange code for token'},
|
||||
)
|
||||
|
||||
token_response = response.json()
|
||||
|
||||
if 'access_token' not in token_response:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={'error': 'No access token in response'},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK,
|
||||
content={'access_token': token_response['access_token']},
|
||||
)
|
||||
|
||||
|
||||
@app.post('/authenticate')
|
||||
async def authenticate(request: Request):
|
||||
token = request.headers.get('X-GitHub-Token')
|
||||
if not await authenticate_github_user(token):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={'error': 'Not authorized via GitHub waitlist'},
|
||||
)
|
||||
|
||||
# Create a signed JWT token with 1-hour expiration
|
||||
cookie_data = {
|
||||
'github_token': token,
|
||||
'exp': int(time.time()) + 3600, # 1 hour expiration
|
||||
}
|
||||
signed_token = sign_token(cookie_data, config.jwt_secret)
|
||||
|
||||
response = JSONResponse(
|
||||
status_code=status.HTTP_200_OK, content={'message': 'User authenticated'}
|
||||
)
|
||||
|
||||
# Set secure cookie with signed token
|
||||
response.set_cookie(
|
||||
key='github_auth',
|
||||
value=signed_token,
|
||||
max_age=3600, # 1 hour in seconds
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite='strict',
|
||||
)
|
||||
return response
|
||||
56
openhands/server/routes/github.py
Normal file
56
openhands/server/routes/github.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import requests
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from openhands.server.shared import openhands_config
|
||||
|
||||
app = APIRouter(prefix='/api')
|
||||
|
||||
|
||||
@app.get('/github/repositories')
|
||||
def get_github_repositories(
|
||||
request: Request,
|
||||
page: int = 1,
|
||||
per_page: int = 10,
|
||||
sort: str = 'pushed',
|
||||
installation_id: int | None = None,
|
||||
):
|
||||
# Extract the GitHub token from the headers
|
||||
github_token = request.headers.get('X-GitHub-Token')
|
||||
if not github_token:
|
||||
raise HTTPException(status_code=400, detail='Missing X-GitHub-Token header')
|
||||
|
||||
openhands_config.verify_github_repo_list(installation_id)
|
||||
|
||||
# Add query parameters
|
||||
params: dict[str, str] = {
|
||||
'page': str(page),
|
||||
'per_page': str(per_page),
|
||||
}
|
||||
# Construct the GitHub API URL
|
||||
if installation_id:
|
||||
github_api_url = (
|
||||
f'https://api.github.com/user/installations/{installation_id}/repositories'
|
||||
)
|
||||
else:
|
||||
github_api_url = 'https://api.github.com/user/repos'
|
||||
params['sort'] = sort
|
||||
|
||||
# Set the authorization header with the GitHub token
|
||||
headers = {
|
||||
'Authorization': f'Bearer {github_token}',
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
}
|
||||
|
||||
# Fetch repositories from GitHub
|
||||
try:
|
||||
response = requests.get(github_api_url, headers=headers, params=params)
|
||||
response.raise_for_status() # Raise an error for HTTP codes >= 400
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=response.status_code if response else 500,
|
||||
detail=f'Error fetching repositories: {str(e)}',
|
||||
)
|
||||
|
||||
# Return the JSON response
|
||||
return JSONResponse(content=response.json())
|
||||
@@ -16,7 +16,7 @@ from openhands.controller.agent import Agent
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.llm import bedrock
|
||||
from openhands.server.shared import config
|
||||
from openhands.server.shared import config, openhands_config
|
||||
|
||||
app = APIRouter(prefix='/api/options')
|
||||
|
||||
@@ -104,3 +104,12 @@ async def get_security_analyzers():
|
||||
list: A sorted list of security analyzer names.
|
||||
"""
|
||||
return sorted(SecurityAnalyzers.keys())
|
||||
|
||||
|
||||
@app.get('/config')
|
||||
async def get_config():
|
||||
"""
|
||||
Get current config
|
||||
"""
|
||||
|
||||
return openhands_config.get_config()
|
||||
|
||||
@@ -16,6 +16,9 @@ from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
|
||||
WAIT_TIME_BEFORE_CLOSE = 300
|
||||
WAIT_TIME_BEFORE_CLOSE_INTERVAL = 5
|
||||
|
||||
|
||||
class AgentSession:
|
||||
"""Represents a session with an Agent
|
||||
@@ -30,6 +33,7 @@ class AgentSession:
|
||||
controller: AgentController | None = None
|
||||
runtime: Runtime | None = None
|
||||
security_analyzer: SecurityAnalyzer | None = None
|
||||
_initializing: bool = False
|
||||
_closed: bool = False
|
||||
loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
@@ -111,6 +115,10 @@ class AgentSession:
|
||||
github_token: str | None = None,
|
||||
selected_repository: str | None = None,
|
||||
):
|
||||
if self._closed:
|
||||
logger.warning('Session closed before starting')
|
||||
return
|
||||
self._initializing = True
|
||||
self._create_security_analyzer(config.security.security_analyzer)
|
||||
await self._create_runtime(
|
||||
runtime_name=runtime_name,
|
||||
@@ -120,7 +128,7 @@ class AgentSession:
|
||||
selected_repository=selected_repository,
|
||||
)
|
||||
|
||||
self._create_controller(
|
||||
self.controller = self._create_controller(
|
||||
agent,
|
||||
config.security.confirmation_mode,
|
||||
max_iterations,
|
||||
@@ -131,9 +139,9 @@ class AgentSession:
|
||||
self.event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.INIT), EventSource.ENVIRONMENT
|
||||
)
|
||||
if self.controller:
|
||||
self.controller.agent_task = self.controller.start_step_loop()
|
||||
await self.controller.agent_task # type: ignore
|
||||
self.controller.agent_task = self.controller.start_step_loop()
|
||||
self._initializing = False
|
||||
await self.controller.agent_task # type: ignore
|
||||
|
||||
def close(self):
|
||||
"""Closes the Agent session"""
|
||||
@@ -143,6 +151,18 @@ class AgentSession:
|
||||
call_async_from_sync(self._close)
|
||||
|
||||
async def _close(self):
|
||||
seconds_waited = 0
|
||||
while self._initializing:
|
||||
logger.debug(
|
||||
f'Waiting for initialization to finish before closing session {self.sid}'
|
||||
)
|
||||
await asyncio.sleep(WAIT_TIME_BEFORE_CLOSE_INTERVAL)
|
||||
seconds_waited += WAIT_TIME_BEFORE_CLOSE_INTERVAL
|
||||
if seconds_waited > WAIT_TIME_BEFORE_CLOSE:
|
||||
logger.error(
|
||||
f'Waited too long for initialization to finish before closing session {self.sid}'
|
||||
)
|
||||
break
|
||||
if self.controller is not None:
|
||||
end_state = self.controller.get_state()
|
||||
end_state.save_to_session(self.sid, self.file_store)
|
||||
@@ -209,18 +229,15 @@ class AgentSession:
|
||||
)
|
||||
return
|
||||
|
||||
if self.runtime is not None:
|
||||
self.runtime.clone_repo(github_token, selected_repository)
|
||||
if agent.prompt_manager:
|
||||
agent.prompt_manager.load_microagent_files(
|
||||
self.runtime.get_custom_microagents(selected_repository)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}'
|
||||
self.runtime.clone_repo(github_token, selected_repository)
|
||||
if agent.prompt_manager:
|
||||
agent.prompt_manager.load_microagent_files(
|
||||
self.runtime.get_custom_microagents(selected_repository)
|
||||
)
|
||||
else:
|
||||
logger.warning('Runtime initialization failed')
|
||||
|
||||
logger.debug(
|
||||
f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}'
|
||||
)
|
||||
|
||||
def _create_controller(
|
||||
self,
|
||||
@@ -230,7 +247,7 @@ class AgentSession:
|
||||
max_budget_per_task: float | None = None,
|
||||
agent_to_llm_config: dict[str, LLMConfig] | None = None,
|
||||
agent_configs: dict[str, AgentConfig] | None = None,
|
||||
):
|
||||
) -> AgentController:
|
||||
"""Creates an AgentController instance
|
||||
|
||||
Parameters:
|
||||
@@ -267,7 +284,7 @@ class AgentSession:
|
||||
)
|
||||
logger.debug(msg)
|
||||
|
||||
self.controller = AgentController(
|
||||
controller = AgentController(
|
||||
sid=self.sid,
|
||||
event_stream=self.event_stream,
|
||||
agent=agent,
|
||||
@@ -281,10 +298,9 @@ class AgentSession:
|
||||
)
|
||||
try:
|
||||
agent_state = State.restore_from_session(self.sid, self.file_store)
|
||||
self.controller.set_initial_state(
|
||||
agent_state, max_iterations, confirmation_mode
|
||||
)
|
||||
controller.set_initial_state(agent_state, max_iterations, confirmation_mode)
|
||||
logger.debug(f'Restored agent state from session, sid: {self.sid}')
|
||||
except Exception as e:
|
||||
logger.debug(f'State could not be restored: {e}')
|
||||
logger.debug('Agent controller initialized.')
|
||||
return controller
|
||||
|
||||
@@ -308,6 +308,7 @@ class SessionManager:
|
||||
)
|
||||
|
||||
await self._close_session(session)
|
||||
return True
|
||||
|
||||
async def _close_session(self, session: Session):
|
||||
logger.info(f'_close_session:{session.sid}')
|
||||
|
||||
@@ -4,12 +4,14 @@ import socketio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from openhands.core.config import load_app_config
|
||||
from openhands.server.config.openhands_config import load_openhands_config
|
||||
from openhands.server.session import SessionManager
|
||||
from openhands.storage import get_file_store
|
||||
|
||||
load_dotenv()
|
||||
|
||||
config = load_app_config()
|
||||
openhands_config = load_openhands_config()
|
||||
file_store = get_file_store(config.file_store, config.file_store_path)
|
||||
|
||||
client_manager = None
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
from google.auth import default
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class GoogleSheetsClient:
|
||||
def __init__(self):
|
||||
"""Initialize Google Sheets client using workload identity.
|
||||
Uses application default credentials which supports workload identity when running in GCP.
|
||||
"""
|
||||
logger.info('Initializing Google Sheets client with workload identity')
|
||||
try:
|
||||
credentials, project = default(
|
||||
scopes=['https://www.googleapis.com/auth/spreadsheets.readonly']
|
||||
)
|
||||
logger.info(f'Successfully obtained credentials for project: {project}')
|
||||
self.service = build('sheets', 'v4', credentials=credentials)
|
||||
logger.info('Successfully initialized Google Sheets API service')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to initialize Google Sheets client: {str(e)}')
|
||||
self.service = None
|
||||
|
||||
def get_usernames(self, spreadsheet_id: str, range_name: str = 'A:A') -> List[str]:
|
||||
"""Get list of usernames from specified Google Sheet.
|
||||
|
||||
Args:
|
||||
spreadsheet_id: The ID of the Google Sheet
|
||||
range_name: The A1 notation of the range to fetch
|
||||
|
||||
Returns:
|
||||
List of usernames from the sheet
|
||||
"""
|
||||
if not self.service:
|
||||
logger.error('Google Sheets service not initialized')
|
||||
return []
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f'Fetching usernames from sheet {spreadsheet_id}, range {range_name}'
|
||||
)
|
||||
result = (
|
||||
self.service.spreadsheets()
|
||||
.values()
|
||||
.get(spreadsheetId=spreadsheet_id, range=range_name)
|
||||
.execute()
|
||||
)
|
||||
|
||||
values = result.get('values', [])
|
||||
usernames = [
|
||||
str(cell[0]).strip() for cell in values if cell and cell[0].strip()
|
||||
]
|
||||
logger.info(
|
||||
f'Successfully fetched {len(usernames)} usernames from Google Sheet'
|
||||
)
|
||||
return usernames
|
||||
|
||||
except HttpError as err:
|
||||
logger.error(f'Error accessing Google Sheet {spreadsheet_id}: {err}')
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Unexpected error accessing Google Sheet {spreadsheet_id}: {str(e)}'
|
||||
)
|
||||
return []
|
||||
42
openhands/server/types.py
Normal file
42
openhands/server/types.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Protocol
|
||||
|
||||
|
||||
class AppMode(Enum):
|
||||
OSS = 'oss'
|
||||
SAAS = 'saas'
|
||||
|
||||
|
||||
class SessionMiddlewareInterface(Protocol):
|
||||
"""Protocol for session middleware classes."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OpenhandsConfigInterface(ABC):
|
||||
CONFIG_PATH: ClassVar[str | None]
|
||||
APP_MODE: ClassVar[AppMode]
|
||||
POSTHOG_CLIENT_KEY: ClassVar[str]
|
||||
GITHUB_CLIENT_ID: ClassVar[str]
|
||||
ATTACH_SESSION_MIDDLEWARE_PATH: ClassVar[str]
|
||||
|
||||
@abstractmethod
|
||||
def verify_config(self) -> None:
|
||||
"""Verify configuration settings."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def verify_github_repo_list(self, installation_id: int | None) -> None:
|
||||
"""Verify that repo list is being called via user's profile or Github App installations."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_config(self) -> dict[str, str]:
|
||||
"""Configure attributes for frontend"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def github_auth(self, data: dict) -> None:
|
||||
"""Handle GitHub authentication."""
|
||||
raise NotImplementedError
|
||||
550
poetry.lock
generated
550
poetry.lock
generated
@@ -170,13 +170,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.39.0"
|
||||
version = "0.40.0"
|
||||
description = "The official Python library for the anthropic API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "anthropic-0.39.0-py3-none-any.whl", hash = "sha256:ea17093ae0ce0e1768b0c46501d6086b5bcd74ff39d68cd2d6396374e9de7c09"},
|
||||
{file = "anthropic-0.39.0.tar.gz", hash = "sha256:94671cc80765f9ce693f76d63a97ee9bef4c2d6063c044e983d21a2e262f63ba"},
|
||||
{file = "anthropic-0.40.0-py3-none-any.whl", hash = "sha256:442028ae8790ff9e3b6f8912043918755af1230d193904ae2ef78cc22995280c"},
|
||||
{file = "anthropic-0.40.0.tar.gz", hash = "sha256:3efeca6d9e97813f93ed34322c6c7ea2279bf0824cd0aa71b59ce222665e2b87"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -553,17 +553,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.35.78"
|
||||
version = "1.35.82"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "boto3-1.35.78-py3-none-any.whl", hash = "sha256:5ef7166fe5060637b92af8dc152cd7acecf96b3fc9c5456706a886cadb534391"},
|
||||
{file = "boto3-1.35.78.tar.gz", hash = "sha256:fc8001519c8842e766ad3793bde3fbd0bb39e821a582fc12cf67876b8f3cf7f1"},
|
||||
{file = "boto3-1.35.82-py3-none-any.whl", hash = "sha256:c422b68ae76959b9e23b77eb79e41c3483332f7e1de918d2b083c456d8cf234c"},
|
||||
{file = "boto3-1.35.82.tar.gz", hash = "sha256:2bbaf1551b1ed55770cb437d7040f1abe6742601103695057b30ce6328eef286"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.35.78,<1.36.0"
|
||||
botocore = ">=1.35.82,<1.36.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
@@ -572,13 +572,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.35.78"
|
||||
version = "1.35.82"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "botocore-1.35.78-py3-none-any.whl", hash = "sha256:41c37bd7c0326f25122f33ec84fb80fc0a14d7fcc9961431b0e57568e88c9cb5"},
|
||||
{file = "botocore-1.35.78.tar.gz", hash = "sha256:6905036c25449ae8dba5e950e4b908e4b8a6fe6b516bf61e007ecb62fa21f323"},
|
||||
{file = "botocore-1.35.82-py3-none-any.whl", hash = "sha256:e43b97d8cbf19d35ce3a177f144bd97cc370f0a67d0984c7d7cf105ac198748f"},
|
||||
{file = "botocore-1.35.82.tar.gz", hash = "sha256:78dd7bf8f49616d00073698d7bbaf5a115208fe730b7b7afae4456adddb3552e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1632,13 +1632,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.5"
|
||||
version = "0.115.6"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"},
|
||||
{file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"},
|
||||
{file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"},
|
||||
{file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2175,13 +2175,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.154.0"
|
||||
version = "2.155.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"},
|
||||
{file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"},
|
||||
{file = "google_api_python_client-2.155.0-py2.py3-none-any.whl", hash = "sha256:83fe9b5aa4160899079d7c93a37be306546a17e6686e2549bcc9584f1a229747"},
|
||||
{file = "google_api_python_client-2.155.0.tar.gz", hash = "sha256:25529f89f0d13abcf3c05c089c423fb2858ac16e0b3727543393468d0d7af67c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2249,13 +2249,13 @@ tool = ["click (>=6.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-aiplatform"
|
||||
version = "1.73.0"
|
||||
version = "1.74.0"
|
||||
description = "Vertex AI API client library"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "google_cloud_aiplatform-1.73.0-py2.py3-none-any.whl", hash = "sha256:6f9aebc1cb2277048093f17214c5f4ec9129fa347b8b22d784f780b12b8865a9"},
|
||||
{file = "google_cloud_aiplatform-1.73.0.tar.gz", hash = "sha256:687d4d6dd26439db42d38b835ea0da7ebb75c20ca8e17666669536b253637e74"},
|
||||
{file = "google_cloud_aiplatform-1.74.0-py2.py3-none-any.whl", hash = "sha256:7f37a835e543a4cb4b62505928b983e307c5fee6d949f831cd3804f03c753d87"},
|
||||
{file = "google_cloud_aiplatform-1.74.0.tar.gz", hash = "sha256:2202e4e0cbbd2db02835737a1ae9a51ad7bf75c8ed130a3fdbcfced33525e3f0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2279,7 +2279,7 @@ endpoint = ["requests (>=2.28.1)"]
|
||||
evaluation = ["pandas (>=1.0.0)", "tqdm (>=4.23.0)"]
|
||||
full = ["docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.114.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.16.0)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "requests (>=2.28.1)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)"]
|
||||
langchain = ["langchain (>=0.1.16,<0.4)", "langchain-core (<0.4)", "langchain-google-vertexai (<3)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)"]
|
||||
langchain-testing = ["absl-py", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "langchain (>=0.1.16,<0.4)", "langchain-core (<0.4)", "langchain-google-vertexai (<3)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<3)", "pytest-xdist"]
|
||||
langchain-testing = ["absl-py", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "langchain (>=0.1.16,<0.4)", "langchain-core (<0.4)", "langchain-google-vertexai (<3)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<2.10)", "pytest-xdist"]
|
||||
lit = ["explainable-ai-sdk (>=1.0.0)", "lit-nlp (==0.4.0)", "pandas (>=1.0.0)", "tensorflow (>=2.3.0,<3.0.0dev)"]
|
||||
metadata = ["numpy (>=1.15.0)", "pandas (>=1.0.0)"]
|
||||
pipelines = ["pyyaml (>=5.3.1,<7)"]
|
||||
@@ -2287,7 +2287,7 @@ prediction = ["docker (>=5.0.3)", "fastapi (>=0.71.0,<=0.114.0)", "httpx (>=0.23
|
||||
private-endpoints = ["requests (>=2.28.1)", "urllib3 (>=1.21.1,<1.27)"]
|
||||
ray = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "setuptools (<70.0.0)"]
|
||||
ray-testing = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "pytest-xdist", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "ray[train]", "scikit-learn", "setuptools (<70.0.0)", "tensorflow", "torch (>=2.0.0,<2.1.0)", "xgboost", "xgboost-ray"]
|
||||
reasoningengine = ["cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<3)"]
|
||||
reasoningengine = ["cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<2.10)"]
|
||||
tensorboard = ["tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"]
|
||||
testing = ["aiohttp", "bigframes", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.114.0)", "google-api-core (>=2.11,<3.0.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "grpcio-testing", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "ipython", "kfp (>=2.6.0,<3.0.0)", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.16.0)", "nltk", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pytest-asyncio", "pytest-xdist", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "requests (>=2.28.1)", "requests-toolbelt (<1.0.0)", "scikit-learn", "sentencepiece (>=0.2.0)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (==2.13.0)", "tensorflow (==2.16.1)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "torch (>=2.0.0,<2.1.0)", "torch (>=2.2.0)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)", "xgboost"]
|
||||
tokenization = ["sentencepiece (>=0.2.0)"]
|
||||
@@ -3256,13 +3256,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "json-repair"
|
||||
version = "0.30.2"
|
||||
version = "0.31.0"
|
||||
description = "A package to repair broken json strings"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "json_repair-0.30.2-py3-none-any.whl", hash = "sha256:824d7ab208f5daadf7925e362979870ba91f9a5e6242d59f7073c7171abe27b5"},
|
||||
{file = "json_repair-0.30.2.tar.gz", hash = "sha256:aa244f0d91e81c9587b2b6981a5657a7d4fadb20051f74bc6110f1ca6a963fb9"},
|
||||
{file = "json_repair-0.31.0-py3-none-any.whl", hash = "sha256:82a4f60a9a836ed12b103367477af96f94fc6a14310d70a06e5354c3eb287d11"},
|
||||
{file = "json_repair-0.31.0.tar.gz", hash = "sha256:3539dbb1857fa2c64404e1cf3e53b091738e74fcf3f12762d4199a0e2af657f5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3494,13 +3494,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
|
||||
|
||||
[[package]]
|
||||
name = "jupyterlab"
|
||||
version = "4.3.1"
|
||||
version = "4.3.3"
|
||||
description = "JupyterLab computational environment"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jupyterlab-4.3.1-py3-none-any.whl", hash = "sha256:2d9a1c305bc748e277819a17a5d5e22452e533e835f4237b2f30f3b0e491e01f"},
|
||||
{file = "jupyterlab-4.3.1.tar.gz", hash = "sha256:a4a338327556443521731d82f2a6ccf926df478914ca029616621704d47c3c65"},
|
||||
{file = "jupyterlab-4.3.3-py3-none-any.whl", hash = "sha256:32a8fd30677e734ffcc3916a4758b9dab21b02015b668c60eb36f84357b7d4b1"},
|
||||
{file = "jupyterlab-4.3.3.tar.gz", hash = "sha256:76fa39e548fdac94dc1204af5956c556f54c785f70ee26aa47ea08eda4d5bbcd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3514,7 +3514,7 @@ jupyter-server = ">=2.4.0,<3"
|
||||
jupyterlab-server = ">=2.27.1,<3"
|
||||
notebook-shim = ">=0.2"
|
||||
packaging = "*"
|
||||
setuptools = ">=40.1.0"
|
||||
setuptools = ">=40.8.0"
|
||||
tornado = ">=6.2.0"
|
||||
traitlets = "*"
|
||||
|
||||
@@ -3739,13 +3739,13 @@ types-tqdm = "*"
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.54.1"
|
||||
version = "1.55.3"
|
||||
description = "Library to easily interface with LLM API providers"
|
||||
optional = false
|
||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||
files = [
|
||||
{file = "litellm-1.54.1-py3-none-any.whl", hash = "sha256:d8e60d4a5e8decb0234a1e8c20351c904aec561fb4025df7df3d0d7ea81ca442"},
|
||||
{file = "litellm-1.54.1.tar.gz", hash = "sha256:b5a8fc99160fab0699b9258457432b3975499218ffcf1b515709808b2ce5a2d7"},
|
||||
{file = "litellm-1.55.3-py3-none-any.whl", hash = "sha256:83e48a6104f2b30edb62f708ca47dfd42f22416ed2ab34702770b3a57d866aa5"},
|
||||
{file = "litellm-1.55.3.tar.gz", hash = "sha256:83f7fd5c7b6eec1d431e786be9d73fbb72f1f1bc9aff333169a4a8eb61e72018"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4420,51 +4420,45 @@ tests = ["pytest", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.9.2"
|
||||
version = "3.10.0"
|
||||
description = "Python plotting package"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"},
|
||||
{file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"},
|
||||
{file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"},
|
||||
{file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"},
|
||||
{file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"},
|
||||
{file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"},
|
||||
{file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"},
|
||||
{file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"},
|
||||
{file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"},
|
||||
{file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"},
|
||||
{file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"},
|
||||
{file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"},
|
||||
{file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"},
|
||||
{file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"},
|
||||
{file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"},
|
||||
{file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"},
|
||||
{file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"},
|
||||
{file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"},
|
||||
{file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"},
|
||||
{file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"},
|
||||
{file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"},
|
||||
{file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"},
|
||||
{file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"},
|
||||
{file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"},
|
||||
{file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"},
|
||||
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"},
|
||||
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"},
|
||||
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"},
|
||||
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"},
|
||||
{file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"},
|
||||
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"},
|
||||
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"},
|
||||
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"},
|
||||
{file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4479,7 +4473,7 @@ pyparsing = ">=2.3.1"
|
||||
python-dateutil = ">=2.7"
|
||||
|
||||
[package.extras]
|
||||
dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"]
|
||||
dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
@@ -4519,13 +4513,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "minio"
|
||||
version = "7.2.11"
|
||||
version = "7.2.12"
|
||||
description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "minio-7.2.11-py3-none-any.whl", hash = "sha256:153582ed52ff3b5005ba558e1f25bfe1e9e834f7f0745e594777f28e3e81e1a0"},
|
||||
{file = "minio-7.2.11.tar.gz", hash = "sha256:4db95a21fe1e2022ec975292d8a1f83bd5b18f830d23d42a4518ac7a5281d7c5"},
|
||||
{file = "minio-7.2.12-py3-none-any.whl", hash = "sha256:4b63370ca83f82c23e6fb0a094a1e2b08b275884ae43f6a90c4388a45633e3f5"},
|
||||
{file = "minio-7.2.12.tar.gz", hash = "sha256:2a3fcf4ab753824de8ae3ffeb14da33d6ad416f83a7e82363a27b34da8e91f27"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4661,12 +4655,12 @@ type = ["mypy (==1.11.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "modal"
|
||||
version = "0.66.43"
|
||||
version = "0.68.26"
|
||||
description = "Python client library for Modal"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "modal-0.66.43-py3-none-any.whl", hash = "sha256:4314f1c5f3945078109bc300d7ed0d1c954d7f9cdcfc1dcadd5307b46c0bb5f6"},
|
||||
{file = "modal-0.68.26-py3-none-any.whl", hash = "sha256:ddc433ec699493b69d30e05e2e7747548b452ed13eb5cba26622da3ae2a61ad1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4677,7 +4671,7 @@ fastapi = "*"
|
||||
grpclib = "0.4.7"
|
||||
protobuf = ">=3.19,<4.24.0 || >4.24.0,<6.0"
|
||||
rich = ">=12.0.0"
|
||||
synchronicity = ">=0.9.3,<0.10.0"
|
||||
synchronicity = ">=0.9.6,<0.10.0"
|
||||
toml = "*"
|
||||
typer = ">=0.9"
|
||||
types-certifi = "*"
|
||||
@@ -5102,26 +5096,26 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "notebook"
|
||||
version = "7.0.7"
|
||||
version = "7.3.1"
|
||||
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"},
|
||||
{file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"},
|
||||
{file = "notebook-7.3.1-py3-none-any.whl", hash = "sha256:212e1486b2230fe22279043f33c7db5cf9a01d29feb063a85cb139747b7c9483"},
|
||||
{file = "notebook-7.3.1.tar.gz", hash = "sha256:84381c2a82d867517fd25b86e986dae1fe113a70b98f03edff9b94e499fec8fa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jupyter-server = ">=2.4.0,<3"
|
||||
jupyterlab = ">=4.0.2,<5"
|
||||
jupyterlab-server = ">=2.22.1,<3"
|
||||
jupyterlab = ">=4.3.2,<4.4"
|
||||
jupyterlab-server = ">=2.27.1,<3"
|
||||
notebook-shim = ">=0.2,<0.3"
|
||||
tornado = ">=6.2.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["hatch", "pre-commit"]
|
||||
docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
|
||||
test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"]
|
||||
test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "notebook-shim"
|
||||
@@ -5142,66 +5136,66 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.1.3"
|
||||
version = "2.2.0"
|
||||
description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"},
|
||||
{file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"},
|
||||
{file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"},
|
||||
{file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"},
|
||||
{file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"},
|
||||
{file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"},
|
||||
{file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"},
|
||||
{file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"},
|
||||
{file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"},
|
||||
{file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"},
|
||||
{file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"},
|
||||
{file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"},
|
||||
{file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"},
|
||||
{file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"},
|
||||
{file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"},
|
||||
{file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"},
|
||||
{file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"},
|
||||
{file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"},
|
||||
{file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"},
|
||||
{file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"},
|
||||
{file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5429,13 +5423,13 @@ sympy = "*"
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.57.2"
|
||||
version = "1.57.4"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "openai-1.57.2-py3-none-any.whl", hash = "sha256:f7326283c156fdee875746e7e54d36959fb198eadc683952ee05e3302fbd638d"},
|
||||
{file = "openai-1.57.2.tar.gz", hash = "sha256:5f49fd0f38e9f2131cda7deb45dafdd1aee4f52a637e190ce0ecf40147ce8cee"},
|
||||
{file = "openai-1.57.4-py3-none-any.whl", hash = "sha256:7def1ab2d52f196357ce31b9cfcf4181529ce00838286426bb35be81c035dafb"},
|
||||
{file = "openai-1.57.4.tar.gz", hash = "sha256:a8f071a3e9198e2818f63aade68e759417b9f62c0971bdb83de82504b70b77f7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -6338,53 +6332,53 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pyarrow"
|
||||
version = "18.0.0"
|
||||
version = "18.1.0"
|
||||
description = "Python library for Apache Arrow"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2333f93260674e185cfbf208d2da3007132572e56871f451ba1a556b45dae6e2"},
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4c381857754da44326f3a49b8b199f7f87a51c2faacd5114352fc78de30d3aba"},
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:603cd8ad4976568954598ef0a6d4ed3dfb78aff3d57fa8d6271f470f0ce7d34f"},
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58a62549a3e0bc9e03df32f350e10e1efb94ec6cf63e3920c3385b26663948ce"},
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bc97316840a349485fbb137eb8d0f4d7057e1b2c1272b1a20eebbbe1848f5122"},
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2e549a748fa8b8715e734919923f69318c953e077e9c02140ada13e59d043310"},
|
||||
{file = "pyarrow-18.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:606e9a3dcb0f52307c5040698ea962685fb1c852d72379ee9412be7de9c5f9e2"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d5795e37c0a33baa618c5e054cd61f586cf76850a251e2b21355e4085def6280"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:5f0510608ccd6e7f02ca8596962afb8c6cc84c453e7be0da4d85f5f4f7b0328a"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616ea2826c03c16e87f517c46296621a7c51e30400f6d0a61be645f203aa2b93"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1824f5b029ddd289919f354bc285992cb4e32da518758c136271cf66046ef22"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6dd1b52d0d58dd8f685ced9971eb49f697d753aa7912f0a8f50833c7a7426319"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:320ae9bd45ad7ecc12ec858b3e8e462578de060832b98fc4d671dee9f10d9954"},
|
||||
{file = "pyarrow-18.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:2c992716cffb1088414f2b478f7af0175fd0a76fea80841b1706baa8fb0ebaad"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e7ab04f272f98ebffd2a0661e4e126036f6936391ba2889ed2d44c5006237802"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:03f40b65a43be159d2f97fd64dc998f769d0995a50c00f07aab58b0b3da87e1f"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be08af84808dff63a76860847c48ec0416928a7b3a17c2f49a072cac7c45efbd"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70c1965cde991b711a98448ccda3486f2a336457cf4ec4dca257a926e149c9"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00178509f379415a3fcf855af020e3340254f990a8534294ec3cf674d6e255fd"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a71ab0589a63a3e987beb2bc172e05f000a5c5be2636b4b263c44034e215b5d7"},
|
||||
{file = "pyarrow-18.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe92efcdbfa0bcf2fa602e466d7f2905500f33f09eb90bf0bcf2e6ca41b574c8"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:907ee0aa8ca576f5e0cdc20b5aeb2ad4d3953a3b4769fc4b499e00ef0266f02f"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:66dcc216ebae2eb4c37b223feaf82f15b69d502821dde2da138ec5a3716e7463"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1daf7c425f58527900876354390ee41b0ae962a73ad0959b9d829def583bb1"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871b292d4b696b09120ed5bde894f79ee2a5f109cb84470546471df264cae136"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:082ba62bdcb939824ba1ce10b8acef5ab621da1f4c4805e07bfd153617ac19d4"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c664ab88b9766413197733c1720d3dcd4190e8fa3bbdc3710384630a0a7207b"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc892be34dbd058e8d189b47db1e33a227d965ea8805a235c8a7286f7fd17d3a"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:28f9c39a56d2c78bf6b87dcc699d520ab850919d4a8c7418cd20eda49874a2ea"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:f1a198a50c409ab2d009fbf20956ace84567d67f2c5701511d4dd561fae6f32e"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5bd7fd32e3ace012d43925ea4fc8bd1b02cc6cc1e9813b518302950e89b5a22"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336addb8b6f5208be1b2398442c703a710b6b937b1a046065ee4db65e782ff5a"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:45476490dd4adec5472c92b4d253e245258745d0ccaabe706f8d03288ed60a79"},
|
||||
{file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b46591222c864e7da7faa3b19455196416cd8355ff6c2cc2e65726a760a3c420"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:eb7e3abcda7e1e6b83c2dc2909c8d045881017270a119cc6ee7fdcfe71d02df8"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:09f30690b99ce34e0da64d20dab372ee54431745e4efb78ac938234a282d15f9"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5ca5d707e158540312e09fd907f9f49bacbe779ab5236d9699ced14d2293b8"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6331f280c6e4521c69b201a42dd978f60f7e129511a55da9e0bfe426b4ebb8d"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3ac24b2be732e78a5a3ac0b3aa870d73766dd00beba6e015ea2ea7394f8b4e55"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b30a927c6dff89ee702686596f27c25160dd6c99be5bcc1513a763ae5b1bfc03"},
|
||||
{file = "pyarrow-18.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f40ec677e942374e3d7f2fad6a67a4c2811a8b975e8703c6fd26d3b168a90e2"},
|
||||
{file = "pyarrow-18.0.0.tar.gz", hash = "sha256:a6aa027b1a9d2970cf328ccd6dbe4a996bc13c39fd427f502782f5bdb9ca20f5"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56"},
|
||||
{file = "pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"},
|
||||
{file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30"},
|
||||
{file = "pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9"},
|
||||
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b331e477e40f07238adc7ba7469c36b908f07c89b95dd4bd3a0ec84a3d1e21e"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2c4dd0c9010a25ba03e198fe743b1cc03cd33c08190afff371749c52ccbbaf76"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f97b31b4c4e21ff58c6f330235ff893cc81e23da081b1a4b1c982075e0ed4e9"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4813cb8ecf1809871fd2d64a8eff740a1bd3691bbe55f01a3cf6c5ec869754"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:05a5636ec3eb5cc2a36c6edb534a38ef57b2ab127292a716d00eabb887835f1e"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73eeed32e724ea3568bb06161cad5fa7751e45bc2228e33dcb10c614044165c7"},
|
||||
{file = "pyarrow-18.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a1880dd6772b685e803011a6b43a230c23b566859a6e0c9a276c1e0faf4f4052"},
|
||||
{file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -6698,13 +6692,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.10.0"
|
||||
version = "2.10.1"
|
||||
description = "JSON Web Token implementation in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "PyJWT-2.10.0-py3-none-any.whl", hash = "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15"},
|
||||
{file = "pyjwt-2.10.0.tar.gz", hash = "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c"},
|
||||
{file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
|
||||
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -6872,13 +6866,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -6892,20 +6886,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.24.0"
|
||||
version = "0.25.0"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
|
||||
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
|
||||
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
|
||||
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=8.2,<9"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
|
||||
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -7054,13 +7048,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.17"
|
||||
version = "0.0.20"
|
||||
description = "A streaming multipart parser for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
|
||||
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
|
||||
{file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
|
||||
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7338,13 +7332,13 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "5.2.0"
|
||||
version = "5.2.1"
|
||||
description = "Python client for Redis database and key-value store"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
|
||||
{file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
|
||||
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
|
||||
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -7702,40 +7696,40 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.0"
|
||||
version = "0.8.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"},
|
||||
{file = "ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b"},
|
||||
{file = "ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3"},
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"},
|
||||
{file = "ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2"},
|
||||
{file = "ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70"},
|
||||
{file = "ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd"},
|
||||
{file = "ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426"},
|
||||
{file = "ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468"},
|
||||
{file = "ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f"},
|
||||
{file = "ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6"},
|
||||
{file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"},
|
||||
{file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
|
||||
{file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
|
||||
{file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
|
||||
{file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
|
||||
{file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
|
||||
{file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
|
||||
{file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runloop-api-client"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
description = "The official Python library for the runloop API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "runloop_api_client-0.10.0-py3-none-any.whl", hash = "sha256:306f82a012e4178b16ed44434872c374a66cdf9aeea5657f2aacace28f68c283"},
|
||||
{file = "runloop_api_client-0.10.0.tar.gz", hash = "sha256:d383c317bae6671f10b7828312dd20734c691e284d7a1f79a32d8b280c0e258c"},
|
||||
{file = "runloop_api_client-0.11.0-py3-none-any.whl", hash = "sha256:b2b5d7922375ad4fafae39a6779884b4e1fd334dc2af0a874394c7d2e3bd5a7b"},
|
||||
{file = "runloop_api_client-0.11.0.tar.gz", hash = "sha256:c6a332421e0d469d3331ef150fba2970f499c3ecc8ae0f3e0dd69292858e0eab"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -8376,13 +8370,13 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7
|
||||
|
||||
[[package]]
|
||||
name = "streamlit"
|
||||
version = "1.40.1"
|
||||
version = "1.41.1"
|
||||
description = "A faster way to build and share data apps"
|
||||
optional = false
|
||||
python-versions = "!=3.9.7,>=3.8"
|
||||
python-versions = "!=3.9.7,>=3.9"
|
||||
files = [
|
||||
{file = "streamlit-1.40.1-py2.py3-none-any.whl", hash = "sha256:b9d7a317a0cc88edd7857c7e07dde9cf95647d3ae51cbfa8a3db82fbb8a2990d"},
|
||||
{file = "streamlit-1.40.1.tar.gz", hash = "sha256:1f2b09f04b6ad366a2c7b4d48104697d1c8bc33f48bdf7ed939cc04c12d3aec6"},
|
||||
{file = "streamlit-1.41.1-py2.py3-none-any.whl", hash = "sha256:0def00822480071d642e6df36cd63c089f991da3a69fd9eb4ab8f65ce27de4e0"},
|
||||
{file = "streamlit-1.41.1.tar.gz", hash = "sha256:6626d32b098ba1458b71eebdd634c62af2dd876380e59c4b6a1e828a39d62d69"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -8391,7 +8385,7 @@ blinker = ">=1.0.0,<2"
|
||||
cachetools = ">=4.0,<6"
|
||||
click = ">=7.0,<9"
|
||||
gitpython = ">=3.0.7,<3.1.19 || >3.1.19,<4"
|
||||
numpy = ">=1.20,<3"
|
||||
numpy = ">=1.23,<3"
|
||||
packaging = ">=20,<25"
|
||||
pandas = ">=1.4.0,<3"
|
||||
pillow = ">=7.1.0,<12"
|
||||
@@ -8487,13 +8481,13 @@ dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "synchronicity"
|
||||
version = "0.9.3"
|
||||
version = "0.9.6"
|
||||
description = "Export blocking and async library versions from a single async implementation"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "synchronicity-0.9.3-py3-none-any.whl", hash = "sha256:73c06fe6613c698cbcfa6e77ab6b8d49cce3494c5afc3ef23b007b1fdff2256d"},
|
||||
{file = "synchronicity-0.9.3.tar.gz", hash = "sha256:d3856601e63e518a143ec42f57988d9e88e4f94716168b717fd4b1b64f4704fd"},
|
||||
{file = "synchronicity-0.9.6-py3-none-any.whl", hash = "sha256:4bcb170500c044004198f2ebbd52bd5c7c318e3862b8be3f0ab7321482babb44"},
|
||||
{file = "synchronicity-0.9.6.tar.gz", hash = "sha256:755c99881700256038939a39a1bbf1052b5da9bcd73524659ba686db0a340254"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -9281,13 +9275,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.32.1"
|
||||
version = "0.34.0"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
|
||||
{file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
|
||||
{file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
|
||||
{file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -10039,48 +10033,48 @@ test = ["zope.testrunner"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-interface"
|
||||
version = "7.1.1"
|
||||
version = "7.2"
|
||||
description = "Interfaces for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "zope.interface-7.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6650bd56ef350d37c8baccfd3ee8a0483ed6f8666e641e4b9ae1a1827b79f9e5"},
|
||||
{file = "zope.interface-7.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84e87eba6b77a3af187bae82d8de1a7c208c2a04ec9f6bd444fd091b811ad92e"},
|
||||
{file = "zope.interface-7.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c4e1b4c06d9abd1037c088dae1566c85f344a3e6ae4350744c3f7f7259d9c67"},
|
||||
{file = "zope.interface-7.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cd5e3d910ac87652a09f6e5db8e41bc3b49cf08ddd2d73d30afc644801492cd"},
|
||||
{file = "zope.interface-7.1.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca95594d936ee349620900be5b46c0122a1ff6ce42d7d5cb2cf09dc84071ef16"},
|
||||
{file = "zope.interface-7.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:ad339509dcfbbc99bf8e147db6686249c4032f26586699ec4c82f6e5909c9fe2"},
|
||||
{file = "zope.interface-7.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e59f175e868f856a77c0a77ba001385c377df2104fdbda6b9f99456a01e102a"},
|
||||
{file = "zope.interface-7.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0de23bcb93401994ea00bc5c677ef06d420340ac0a4e9c10d80e047b9ce5af3f"},
|
||||
{file = "zope.interface-7.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdb7e7e5524b76d3ec037c1d81a9e2c7457b240fd4cb0a2476b65c3a5a6c81f"},
|
||||
{file = "zope.interface-7.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3603ef82a9920bd0bfb505423cb7e937498ad971ad5a6141841e8f76d2fd5446"},
|
||||
{file = "zope.interface-7.1.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d52d052355e0c5c89e0630dd2ff7c0b823fd5f56286a663e92444761b35e25"},
|
||||
{file = "zope.interface-7.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:179ad46ece518c9084cb272e4a69d266b659f7f8f48e51706746c2d8a426433e"},
|
||||
{file = "zope.interface-7.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6503534b52bb1720ace9366ee30838a58a3413d3e197512f3338c8f34b5d89d"},
|
||||
{file = "zope.interface-7.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f85b290e5b8b11814efb0d004d8ce6c9a483c35c462e8d9bf84abb93e79fa770"},
|
||||
{file = "zope.interface-7.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d029fac6a80edae80f79c37e5e3abfa92968fe921886139b3ee470a1b177321a"},
|
||||
{file = "zope.interface-7.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5836b8fb044c6e75ba34dfaabc602493019eadfa0faf6ff25f4c4c356a71a853"},
|
||||
{file = "zope.interface-7.1.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7395f13533318f150ee72adb55b29284b16e73b6d5f02ab21f173b3e83f242b8"},
|
||||
{file = "zope.interface-7.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d0e23c6b746eb8ce04573cc47bcac60961ac138885d207bd6f57e27a1431ae8"},
|
||||
{file = "zope.interface-7.1.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:9fad9bd5502221ab179f13ea251cb30eef7cf65023156967f86673aff54b53a0"},
|
||||
{file = "zope.interface-7.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:55c373becbd36a44d0c9be1d5271422fdaa8562d158fb44b4192297b3c67096c"},
|
||||
{file = "zope.interface-7.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed1df8cc01dd1e3970666a7370b8bfc7457371c58ba88c57bd5bca17ab198053"},
|
||||
{file = "zope.interface-7.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99c14f0727c978639139e6cad7a60e82b7720922678d75aacb90cf4ef74a068c"},
|
||||
{file = "zope.interface-7.1.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b1eed7670d564f1025d7cda89f99f216c30210e42e95de466135be0b4a499d9"},
|
||||
{file = "zope.interface-7.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:3defc925c4b22ac1272d544a49c6ba04c3eefcce3200319ee1be03d9270306dd"},
|
||||
{file = "zope.interface-7.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8d0fe45be57b5219aa4b96e846631c04615d5ef068146de5a02ccd15c185321f"},
|
||||
{file = "zope.interface-7.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bcbeb44fc16e0078b3b68a95e43f821ae34dcbf976dde6985141838a5f23dd3d"},
|
||||
{file = "zope.interface-7.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8e7b05dc6315a193cceaec071cc3cf1c180cea28808ccded0b1283f1c38ba73"},
|
||||
{file = "zope.interface-7.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d553e02b68c0ea5a226855f02edbc9eefd99f6a8886fa9f9bdf999d77f46585"},
|
||||
{file = "zope.interface-7.1.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81744a7e61b598ebcf4722ac56a7a4f50502432b5b4dc7eb29075a89cf82d029"},
|
||||
{file = "zope.interface-7.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7720322763aceb5e0a7cadcc38c67b839efe599f0887cbf6c003c55b1458c501"},
|
||||
{file = "zope.interface-7.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ed0852c25950cf430067f058f8d98df6288502ac313861d9803fe7691a9b3"},
|
||||
{file = "zope.interface-7.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9595e478047ce752b35cfa221d7601a5283ccdaab40422e0dc1d4a334c70f580"},
|
||||
{file = "zope.interface-7.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2317e1d4dba68203a5227ea3057f9078ec9376275f9700086b8f0ffc0b358e1b"},
|
||||
{file = "zope.interface-7.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6821ef9870f32154da873fcde439274f99814ea452dd16b99fa0b66345c4b6b"},
|
||||
{file = "zope.interface-7.1.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:190eeec67e023d5aac54d183fa145db0b898664234234ac54643a441da434616"},
|
||||
{file = "zope.interface-7.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:d17e7fc814eaab93409b80819fd6d30342844345c27f3bc3c4b43c2425a8d267"},
|
||||
{file = "zope.interface-7.1.1.tar.gz", hash = "sha256:4284d664ef0ff7b709836d4de7b13d80873dc5faeffc073abdb280058bfac5e3"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b"},
|
||||
{file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"},
|
||||
{file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"},
|
||||
{file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"},
|
||||
{file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"},
|
||||
{file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"},
|
||||
{file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"},
|
||||
{file = "zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7"},
|
||||
{file = "zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465"},
|
||||
{file = "zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89"},
|
||||
{file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54"},
|
||||
{file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d"},
|
||||
{file = "zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5"},
|
||||
{file = "zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98"},
|
||||
{file = "zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d"},
|
||||
{file = "zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c"},
|
||||
{file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398"},
|
||||
{file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b"},
|
||||
{file = "zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd"},
|
||||
{file = "zope.interface-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3a8ffec2a50d8ec470143ea3d15c0c52d73df882eef92de7537e8ce13475e8a"},
|
||||
{file = "zope.interface-7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d06db13a30303c08d61d5fb32154be51dfcbdb8438d2374ae27b4e069aac40"},
|
||||
{file = "zope.interface-7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e204937f67b28d2dca73ca936d3039a144a081fc47a07598d44854ea2a106239"},
|
||||
{file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b7b0314f919e751f2bca17d15aad00ddbb1eadf1cb0190fa8175edb7ede62"},
|
||||
{file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf95683cde5bc7d0e12d8e7588a3eb754d7c4fa714548adcd96bdf90169f021"},
|
||||
{file = "zope.interface-7.2-cp38-cp38-win_amd64.whl", hash = "sha256:7dc5016e0133c1a1ec212fc87a4f7e7e562054549a99c73c8896fa3a9e80cbc7"},
|
||||
{file = "zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb"},
|
||||
{file = "zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7"},
|
||||
{file = "zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137"},
|
||||
{file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519"},
|
||||
{file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75"},
|
||||
{file = "zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d"},
|
||||
{file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -10094,4 +10088,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "7334dd947fe93756227b5fc8f86303852c5e9aaf8787cc35b0ce23fc1540df67"
|
||||
content-hash = "c770c2ef5342d40b2288168e983f40e9fc8d16ced074cf67586c7e923bb6f3f8"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.15.2"
|
||||
version = "0.15.3"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -37,9 +37,9 @@ python-multipart = "*"
|
||||
boto3 = "*"
|
||||
minio = "^7.2.8"
|
||||
gevent = "^24.2.1"
|
||||
pyarrow = "18.0.0" # transitive dependency, pinned here to avoid conflicts
|
||||
pyarrow = "18.1.0" # transitive dependency, pinned here to avoid conflicts
|
||||
tenacity = "^8.5.0"
|
||||
zope-interface = "7.1.1"
|
||||
zope-interface = "7.2"
|
||||
pathspec = "^0.12.1"
|
||||
google-cloud-aiplatform = "*"
|
||||
anthropic = {extras = ["vertex"], version = "*"}
|
||||
@@ -60,11 +60,11 @@ whatthepatch = "^1.0.6"
|
||||
protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+
|
||||
opentelemetry-api = "1.25.0"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
|
||||
modal = "^0.66.26"
|
||||
runloop-api-client = "0.10.0"
|
||||
modal = ">=0.66.26,<0.69.0"
|
||||
runloop-api-client = "0.11.0"
|
||||
pygithub = "^2.5.0"
|
||||
joblib = "*"
|
||||
openhands-aci = "^0.1.2"
|
||||
openhands-aci = "0.1.2"
|
||||
python-socketio = "^5.11.4"
|
||||
redis = "^5.2.0"
|
||||
|
||||
@@ -80,7 +80,7 @@ voyageai = "*"
|
||||
llama-index-embeddings-voyageai = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "0.8.0"
|
||||
ruff = "0.8.3"
|
||||
mypy = "1.13.0"
|
||||
pre-commit = "4.0.1"
|
||||
build = "*"
|
||||
@@ -100,6 +100,7 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -107,7 +108,6 @@ jupyter_kernel_gateway = "*"
|
||||
flake8 = "*"
|
||||
opencv-python = "*"
|
||||
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = [
|
||||
@@ -130,6 +130,7 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from contextlib import contextmanager
|
||||
from typing import Type
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,8 +16,12 @@ config = load_app_config()
|
||||
|
||||
@pytest.fixture
|
||||
def test_llm():
|
||||
# Create a mock config for testing
|
||||
return LLM(config=config.get_llm_config())
|
||||
return _get_llm(LLM)
|
||||
|
||||
|
||||
def _get_llm(type_: Type[LLM]):
|
||||
with _patch_http():
|
||||
return type_(config=config.get_llm_config())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -39,6 +45,18 @@ def mock_response():
|
||||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patch_http():
|
||||
with patch('openhands.llm.llm.requests.get', MagicMock()) as mock_http:
|
||||
mock_http.json.return_value = {
|
||||
'data': [
|
||||
{'model_name': 'some_model'},
|
||||
{'model_name': 'another_model'},
|
||||
]
|
||||
}
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_non_streaming():
|
||||
with patch.object(AsyncLLM, '_call_acompletion') as mock_call_acompletion:
|
||||
@@ -46,7 +64,7 @@ async def test_acompletion_non_streaming():
|
||||
'choices': [{'message': {'content': 'This is a test message.'}}]
|
||||
}
|
||||
mock_call_acompletion.return_value = mock_response
|
||||
test_llm = AsyncLLM(config=config.get_llm_config())
|
||||
test_llm = _get_llm(AsyncLLM)
|
||||
response = await test_llm.async_completion(
|
||||
messages=[{'role': 'user', 'content': 'Hello!'}],
|
||||
stream=False,
|
||||
@@ -60,7 +78,7 @@ async def test_acompletion_non_streaming():
|
||||
async def test_acompletion_streaming(mock_response):
|
||||
with patch.object(StreamingLLM, '_call_acompletion') as mock_call_acompletion:
|
||||
mock_call_acompletion.return_value.__aiter__.return_value = iter(mock_response)
|
||||
test_llm = StreamingLLM(config=config.get_llm_config())
|
||||
test_llm = _get_llm(StreamingLLM)
|
||||
async for chunk in test_llm.async_streaming_completion(
|
||||
messages=[{'role': 'user', 'content': 'Hello!'}], stream=True
|
||||
):
|
||||
@@ -109,7 +127,7 @@ async def test_async_completion_with_user_cancellation(cancel_delay):
|
||||
AsyncLLM, '_call_acompletion', new_callable=AsyncMock
|
||||
) as mock_call_acompletion:
|
||||
mock_call_acompletion.side_effect = mock_acompletion
|
||||
test_llm = AsyncLLM(config=config.get_llm_config())
|
||||
test_llm = _get_llm(AsyncLLM)
|
||||
|
||||
async def cancel_after_delay():
|
||||
print(f'Starting cancel_after_delay with delay {cancel_delay}')
|
||||
@@ -171,7 +189,7 @@ async def test_async_streaming_completion_with_user_cancellation(cancel_after_ch
|
||||
AsyncLLM, '_call_acompletion', new_callable=AsyncMock
|
||||
) as mock_call_acompletion:
|
||||
mock_call_acompletion.return_value = mock_acompletion()
|
||||
test_llm = StreamingLLM(config=config.get_llm_config())
|
||||
test_llm = _get_llm(StreamingLLM)
|
||||
|
||||
received_chunks = []
|
||||
with pytest.raises(UserCancelledError):
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.agent_controller import AgentController
|
||||
from openhands.controller.state.state import TrafficControlState
|
||||
from openhands.controller.state.state import State, TrafficControlState
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.core.main import run_controller
|
||||
from openhands.core.schema import AgentState
|
||||
@@ -41,7 +41,9 @@ def mock_agent():
|
||||
|
||||
@pytest.fixture
|
||||
def mock_event_stream():
|
||||
return MagicMock(spec=EventStream)
|
||||
mock = MagicMock(spec=EventStream)
|
||||
mock.get_latest_event_id.return_value = 0
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -278,7 +280,9 @@ async def test_delegate_step_different_states(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_max_iterations(mock_agent, mock_event_stream):
|
||||
async def test_max_iterations_extension(mock_agent, mock_event_stream):
|
||||
# Test with headless_mode=False - should extend max_iterations
|
||||
initial_state = State(max_iterations=10)
|
||||
controller = AgentController(
|
||||
agent=mock_agent,
|
||||
event_stream=mock_event_stream,
|
||||
@@ -286,18 +290,34 @@ async def test_step_max_iterations(mock_agent, mock_event_stream):
|
||||
sid='test',
|
||||
confirmation_mode=False,
|
||||
headless_mode=False,
|
||||
initial_state=initial_state,
|
||||
)
|
||||
controller.state.agent_state = AgentState.RUNNING
|
||||
controller.state.iteration = 10
|
||||
assert controller.state.traffic_control_state == TrafficControlState.NORMAL
|
||||
|
||||
# Trigger throttling by calling _step() when we hit max_iterations
|
||||
await controller._step()
|
||||
assert controller.state.traffic_control_state == TrafficControlState.THROTTLING
|
||||
assert controller.state.agent_state == AgentState.ERROR
|
||||
|
||||
# Simulate a new user message
|
||||
message_action = MessageAction(content='Test message')
|
||||
message_action._source = EventSource.USER
|
||||
await controller.on_event(message_action)
|
||||
|
||||
# Max iterations should be extended to current iteration + initial max_iterations
|
||||
assert (
|
||||
controller.state.max_iterations == 20
|
||||
) # Current iteration (10 initial because _step() should not have been executed) + initial max_iterations (10)
|
||||
assert controller.state.traffic_control_state == TrafficControlState.NORMAL
|
||||
assert controller.state.agent_state == AgentState.RUNNING
|
||||
|
||||
# Close the controller to clean up
|
||||
await controller.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_max_iterations_headless(mock_agent, mock_event_stream):
|
||||
# Test with headless_mode=True - should NOT extend max_iterations
|
||||
initial_state = State(max_iterations=10)
|
||||
controller = AgentController(
|
||||
agent=mock_agent,
|
||||
event_stream=mock_event_stream,
|
||||
@@ -305,13 +325,24 @@ async def test_step_max_iterations_headless(mock_agent, mock_event_stream):
|
||||
sid='test',
|
||||
confirmation_mode=False,
|
||||
headless_mode=True,
|
||||
initial_state=initial_state,
|
||||
)
|
||||
controller.state.agent_state = AgentState.RUNNING
|
||||
controller.state.iteration = 10
|
||||
assert controller.state.traffic_control_state == TrafficControlState.NORMAL
|
||||
|
||||
# Simulate a new user message
|
||||
message_action = MessageAction(content='Test message')
|
||||
message_action._source = EventSource.USER
|
||||
await controller.on_event(message_action)
|
||||
|
||||
# Max iterations should NOT be extended in headless mode
|
||||
assert controller.state.max_iterations == 10 # Original value unchanged
|
||||
|
||||
# Trigger throttling by calling _step() when we hit max_iterations
|
||||
await controller._step()
|
||||
|
||||
assert controller.state.traffic_control_state == TrafficControlState.THROTTLING
|
||||
# In headless mode, throttling results in an error
|
||||
assert controller.state.agent_state == AgentState.ERROR
|
||||
await controller.close()
|
||||
|
||||
|
||||
62
tests/unit/test_iteration_limit.py
Normal file
62
tests/unit/test_iteration_limit.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.controller.agent_controller import AgentController
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events import EventStream
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.event import EventSource
|
||||
|
||||
|
||||
class DummyAgent:
|
||||
def __init__(self):
|
||||
self.name = 'dummy'
|
||||
self.llm = type(
|
||||
'DummyLLM',
|
||||
(),
|
||||
{'metrics': type('DummyMetrics', (), {'merge': lambda x: None})()},
|
||||
)()
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iteration_limit_extends_on_user_message():
|
||||
# Initialize test components
|
||||
from openhands.storage.memory import InMemoryFileStore
|
||||
|
||||
file_store = InMemoryFileStore()
|
||||
event_stream = EventStream(sid='test', file_store=file_store)
|
||||
agent = DummyAgent()
|
||||
initial_max_iterations = 100
|
||||
controller = AgentController(
|
||||
agent=agent,
|
||||
event_stream=event_stream,
|
||||
max_iterations=initial_max_iterations,
|
||||
sid='test',
|
||||
headless_mode=False,
|
||||
)
|
||||
|
||||
# Set initial state
|
||||
await controller.set_agent_state_to(AgentState.RUNNING)
|
||||
controller.state.iteration = 90 # Close to the limit
|
||||
assert controller.state.max_iterations == initial_max_iterations
|
||||
|
||||
# Simulate user message
|
||||
user_message = MessageAction('test message', EventSource.USER)
|
||||
event_stream.add_event(user_message, EventSource.USER)
|
||||
await asyncio.sleep(0.1) # Give time for event to be processed
|
||||
|
||||
# Verify max_iterations was extended
|
||||
assert controller.state.max_iterations == 90 + initial_max_iterations
|
||||
|
||||
# Simulate more iterations and another user message
|
||||
controller.state.iteration = 180 # Close to new limit
|
||||
user_message2 = MessageAction('another message', EventSource.USER)
|
||||
event_stream.add_event(user_message2, EventSource.USER)
|
||||
await asyncio.sleep(0.1) # Give time for event to be processed
|
||||
|
||||
# Verify max_iterations was extended again
|
||||
assert controller.state.max_iterations == 180 + initial_max_iterations
|
||||
@@ -125,6 +125,15 @@ def test_response_latency_tracking(mock_time, mock_litellm_completion):
|
||||
assert response['id'] == 'test-response-123'
|
||||
assert response['choices'][0]['message']['content'] == 'Test response'
|
||||
|
||||
# To make sure the metrics fail gracefully, set the start/end time to go backwards.
|
||||
mock_time.side_effect = [1000.0, 999.0]
|
||||
llm.completion(messages=[{'role': 'user', 'content': 'Hello!'}])
|
||||
|
||||
# There should now be 2 latencies, the last of which has the value clipped to 0
|
||||
assert len(llm.metrics.response_latencies) == 2
|
||||
latency_record = llm.metrics.response_latencies[-1]
|
||||
assert latency_record.latency == 0.0 # Should be lifted to 0 instead of being -1!
|
||||
|
||||
|
||||
def test_llm_reset():
|
||||
llm = LLM(LLMConfig(model='gpt-4o-mini', api_key='test_key'))
|
||||
|
||||
@@ -60,7 +60,7 @@ async def test_session_is_running_in_cluster():
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch('openhands.server.session.manager._REDIS_POLL_TIMEOUT', 0.05),
|
||||
patch('openhands.server.session.manager._REDIS_POLL_TIMEOUT', 0.1),
|
||||
):
|
||||
async with SessionManager(
|
||||
sio, AppConfig(), InMemoryFileStore()
|
||||
@@ -87,7 +87,7 @@ async def test_init_new_local_session():
|
||||
is_session_running_in_cluster_mock.return_value = False
|
||||
with (
|
||||
patch('openhands.server.session.manager.Session', mock_session),
|
||||
patch('openhands.server.session.manager._REDIS_POLL_TIMEOUT', 0.01),
|
||||
patch('openhands.server.session.manager._REDIS_POLL_TIMEOUT', 0.1),
|
||||
patch(
|
||||
'openhands.server.session.manager.SessionManager._redis_subscribe',
|
||||
AsyncMock(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user