Compare commits

..

3 Commits

Author SHA1 Message Date
mamoodi b46d888105 Merge branch 'main' into mh/rel0230 2025-02-04 13:55:14 -05:00
mamoodi eb76656cc9 Merge branch 'main' into mh/rel0230 2025-02-04 09:53:32 -05:00
mamoodi f1f50c6fa6 Release 0.23.0 2025-02-03 16:12:43 -05:00
33 changed files with 368 additions and 684 deletions
+14
View File
@@ -19,6 +19,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
+56
View File
@@ -41,6 +41,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
with:
@@ -90,6 +104,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
with:
@@ -202,6 +230,20 @@ jobs:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -265,6 +307,20 @@ jobs:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -46,8 +46,9 @@ describe("ConversationCard", () => {
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
const card = screen.getByTestId("conversation-card");
const title = within(card).getByTestId("conversation-card-title");
within(card).getByText("Conversation 1");
expect(title).toHaveValue("Conversation 1");
within(card).getByText(expectedDate);
});
@@ -164,8 +165,10 @@ describe("ConversationCard", () => {
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
expect(title).toBeDisabled();
await clickOnEditButton(user);
expect(title).toBeEnabled();
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
@@ -178,6 +181,7 @@ describe("ConversationCard", () => {
expect(onChangeTitle).toHaveBeenCalledWith("New Conversation Name");
expect(title).toHaveValue("New Conversation Name");
expect(title).toBeDisabled();
});
it("should reset title and not call onChangeTitle when the title is empty", async () => {
@@ -204,27 +208,7 @@ describe("ConversationCard", () => {
expect(title).toHaveValue("Conversation 1");
});
test("clicking the title should trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const title = screen.getByTestId("conversation-card-title");
await user.click(title);
expect(onClick).toHaveBeenCalled();
});
test("clicking the title should not trigger the onClick handler if edit mode", async () => {
test("clicking the title should not trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
@@ -237,8 +221,6 @@ describe("ConversationCard", () => {
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
await user.click(title);
@@ -179,10 +179,9 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const title = within(cards[0]).getByTestId("conversation-card-title");
const card = cards[0];
await clickOnEditButton(user, card);
const title = within(card).getByTestId("conversation-card-title");
await clickOnEditButton(user);
await user.clear(title);
await user.type(title, "Conversation 1 Renamed");
@@ -203,10 +202,7 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const card = cards[0];
await clickOnEditButton(user, card);
const title = within(card).getByTestId("conversation-card-title");
const title = within(cards[0]).getByTestId("conversation-card-title");
await user.click(title);
await user.tab();
@@ -1,16 +1,11 @@
import { screen, within } from "@testing-library/react";
import { UserEvent } from "@testing-library/user-event";
export const clickOnEditButton = async (
user: UserEvent,
container?: HTMLElement,
) => {
const wrapper = container ? within(container) : screen;
const ellipsisButton = wrapper.getByTestId("ellipsis-button");
export const clickOnEditButton = async (user: UserEvent) => {
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = wrapper.getByTestId("context-menu");
const menu = screen.getByTestId("context-menu");
const editButton = within(menu).getByTestId("edit-button");
await user.click(editButton);
@@ -58,10 +58,8 @@ export function ConversationCard({
};
const handleInputClick = (event: React.MouseEvent<HTMLInputElement>) => {
if (titleMode === "edit") {
event.preventDefault();
event.stopPropagation();
}
event.preventDefault();
event.stopPropagation();
};
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
@@ -103,26 +101,17 @@ export function ConversationCard({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 w-full">
{isActive && <span className="w-2 h-2 bg-blue-500 rounded-full" />}
{titleMode === "edit" && (
<input
ref={inputRef}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
type="text"
defaultValue={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
/>
)}
{titleMode === "view" && (
<p
data-testid="conversation-card-title"
className="text-sm leading-6 font-semibold bg-transparent w-full"
>
{title}
</p>
)}
<input
ref={inputRef}
disabled={titleMode === "view"}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
type="text"
defaultValue={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
/>
</div>
<div className="flex items-center gap-2 relative">
+115 -119
View File
@@ -3,7 +3,7 @@
"en": "App",
"ja": "アプリ",
"zh-CN": "应用",
"zh-TW": "應用程式",
"zh-TW": "應用",
"ko-KR": "앱",
"no": "App",
"it": "App",
@@ -33,7 +33,7 @@
"en": "If you tell OpenHands to start a web server, the app will appear here.",
"ja": "OpenHandsにWebサーバーの起動を指示すると、ここにアプリが表示されます。",
"zh-CN": "如果您告诉OpenHands启动Web服务器,应用将在此处显示。",
"zh-TW": "如果您要求 OpenHands 啟動網頁伺服器,應用程式將會顯示在此處。",
"zh-TW": "如果您告訴OpenHands啟動Web伺服器,應用在此處顯示。",
"ko-KR": "OpenHands에게 웹 서버를 시작하도록 지시하면 여기에 앱이 표시됩니다.",
"no": "Hvis du ber OpenHands om å starte en webserver, vil appen vises her.",
"it": "Se chiedi a OpenHands di avviare un server web, l'app apparirà qui.",
@@ -138,7 +138,7 @@
"en": "Waiting for client to become ready...",
"ja": "クライアントの準備を待機中...",
"zh-CN": "等待客户端准备就绪...",
"zh-TW": "正在等待戶端就緒...",
"zh-TW": "等待戶端準備就緒...",
"ko-KR": "클라이언트 준비를 기다리는 중...",
"de": "Warte auf Bereitschaft des Clients...",
"no": "Venter på at klienten skal bli klar...",
@@ -227,7 +227,7 @@
"CODE_EDITOR$EMPTY_MESSAGE": {
"en": "No file selected.",
"zh-CN": "未选择文件",
"zh-TW": "未選擇檔案",
"zh-TW": "未選擇檔案",
"de": "Keine Datei ausgewählt.",
"ko-KR": "선택된 파일이 없습니다",
"no": "Ingen fil valgt.",
@@ -245,7 +245,7 @@
"de": "Fehler beim Auswählen der Datei. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 선택 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved valg av fil. Vennligst prøv igjen.",
"zh-TW": "選擇檔案時發生錯誤,請再試一次",
"zh-TW": "選擇檔案時出錯。請重試。",
"it": "Errore durante la selezione del file. Riprova.",
"pt": "Erro ao selecionar o arquivo. Por favor, tente novamente.",
"es": "Error al seleccionar el archivo. Por favor, inténtelo de nuevo.",
@@ -260,7 +260,7 @@
"de": "Fehler beim Hochladen der Dateien. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 업로드 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved opplasting av filer. Vennligst prøv igjen.",
"zh-TW": "上傳檔案時發生錯誤,請再試一次",
"zh-TW": "上傳檔案時出錯。請重試。",
"it": "Errore durante il caricamento dei file. Riprova.",
"pt": "Erro ao fazer upload dos arquivos. Por favor, tente novamente.",
"es": "Error al subir los archivos. Por favor, inténtelo de nuevo.",
@@ -275,7 +275,7 @@
"de": "Fehler beim Auflisten der Dateien. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 목록을 가져오는 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved listing av filer. Vennligst prøv igjen.",
"zh-TW": "列出檔案時發生錯誤,請再試一次",
"zh-TW": "列出檔案時出錯。請重試。",
"it": "Errore durante l'elenco dei file. Riprova.",
"pt": "Erro ao listar arquivos. Por favor, tente novamente.",
"es": "Error al listar los archivos. Por favor, inténtelo de nuevo.",
@@ -290,7 +290,7 @@
"de": "Fehler beim Speichern der Datei. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 저장 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved lagring av fil. Vennligst prøv igjen.",
"zh-TW": "儲存檔案時發生錯誤,請再試一次",
"zh-TW": "儲存檔案時出錯。請重試。",
"it": "Errore durante il salvataggio del file. Riprova.",
"pt": "Erro ao salvar o arquivo. Por favor, tente novamente.",
"es": "Error al guardar el archivo. Por favor, inténtelo de nuevo.",
@@ -318,7 +318,7 @@
"en": "Auto-merge Dependabot PRs",
"ja": "Dependabot PRを自動マージ",
"zh-CN": "自动合并Dependabot PR",
"zh-TW": "自動合併 Dependabot PR",
"zh-TW": "自動合併Dependabot PR",
"ko-KR": "Dependabot PR 자동 병합",
"de": "Dependabot PRs automatisch zusammenführen",
"no": "Auto-flett Dependabot PRs",
@@ -333,7 +333,7 @@
"en": "Improve README",
"ja": "READMEを改善",
"zh-CN": "改进README",
"zh-TW": "改README",
"zh-TW": "改README",
"ko-KR": "README 개선",
"de": "README verbessern",
"no": "Forbedre README",
@@ -348,7 +348,7 @@
"en": "Clean up dependencies",
"ja": "依存関係を整理",
"zh-CN": "清理依赖项",
"zh-TW": "清理相依性",
"zh-TW": "清理依賴項",
"ko-KR": "의존성 정리",
"de": "Abhängigkeiten bereinigen",
"no": "Rydd opp i avhengigheter",
@@ -365,7 +365,7 @@
"de": "Ungültiger Dateipfad. Bitte überprüfen Sie den Dateinamen und versuchen Sie es erneut.",
"ko-KR": "잘못된 파일 경로입니다. 파일 이름을 확인하고 다시 시도해 주세요.",
"no": "Ugyldig filbane. Vennligst sjekk filnavnet og prøv igjen.",
"zh-TW": "無效的檔案路徑請檢查檔案名稱後再試一次",
"zh-TW": "檔案路徑無效。請檢查檔案名稱並重試。",
"it": "Percorso del file non valido. Controlla il nome del file e riprova.",
"pt": "Caminho de arquivo inválido. Por favor, verifique o nome do arquivo e tente novamente.",
"es": "Ruta de archivo inválida. Por favor, verifique el nombre del archivo e inténtelo de nuevo.",
@@ -380,7 +380,7 @@
"de": "Planer",
"ko-KR": "플래너",
"no": "Planlegger",
"zh-TW": "規劃工具",
"zh-TW": "規劃",
"ar": "المخطط",
"fr": "Planificateur",
"it": "Pianificatore",
@@ -438,7 +438,7 @@
"en": "Open in VS Code",
"ja": "VS Codeで開く",
"zh-CN": "在VS Code中打开",
"zh-TW": "在 VS Code 中開啟",
"zh-TW": "在VS Code中開啟",
"ko-KR": "VS Code에서 열기",
"de": "In VS Code öffnen",
"no": "Åpne i VS Code",
@@ -468,7 +468,7 @@
"en": "Auto-merge PRs",
"ja": "PRを自動マージ",
"zh-CN": "自动合并PR",
"zh-TW": "自動合併 PR",
"zh-TW": "自動合併PR",
"ko-KR": "PR 자동 병합",
"de": "PRs automatisch zusammenführen",
"no": "Auto-flett PRer",
@@ -483,7 +483,7 @@
"en": "Fix README",
"ja": "READMEを修正",
"zh-CN": "修复README",
"zh-TW": "修README",
"zh-TW": "修README",
"ko-KR": "README 수정",
"de": "README korrigieren",
"no": "Fiks README",
@@ -515,7 +515,7 @@
"de": "OpenHands Arbeitsverzeichnis",
"ko-KR": "OpenHands 워크스페이스 디렉토리",
"no": "OpenHands arbeidsmappe",
"zh-TW": "OpenHands 工作區目錄",
"zh-TW": "OpenHands工作區目錄",
"it": "Directory dell'area di lavoro OpenHands",
"pt": "Diretório do espaço de trabalho OpenHands",
"es": "Directorio del espacio de trabajo de OpenHands",
@@ -528,7 +528,7 @@
"en": "LLM Provider",
"ja": "LLMプロバイダー",
"zh-CN": "LLM提供商",
"zh-TW": "LLM 供應商",
"zh-TW": "LLM提供者",
"ko-KR": "LLM 제공자",
"no": "LLM-leverandør",
"it": "Provider LLM",
@@ -543,7 +543,7 @@
"en": "Select a provider",
"ja": "プロバイダーを選択",
"zh-CN": "选择提供商",
"zh-TW": "選擇供應商",
"zh-TW": "選擇提供者",
"ko-KR": "제공자 선택",
"no": "Velg en leverandør",
"it": "Seleziona un provider",
@@ -558,7 +558,7 @@
"en": "API Key",
"ja": "APIキー",
"zh-CN": "API密钥",
"zh-TW": "API 金鑰",
"zh-TW": "API金鑰",
"ko-KR": "API 키",
"no": "API-nøkkel",
"it": "Chiave API",
@@ -573,7 +573,7 @@
"en": "Don't know your API key?",
"ja": "APIキーがわかりませんか?",
"zh-CN": "不知道您的API密钥?",
"zh-TW": "不知道您的 API 金鑰?",
"zh-TW": "不知道您的API金鑰?",
"ko-KR": "API 키를 모르시나요?",
"no": "Kjenner du ikke API-nøkkelen din?",
"it": "Non conosci la tua chiave API?",
@@ -618,7 +618,7 @@
"en": "Reset to defaults",
"ja": "デフォルトにリセット",
"zh-CN": "重置为默认值",
"zh-TW": "還原為預設值",
"zh-TW": "重置為預設值",
"ko-KR": "기본값으로 재설정",
"no": "Tilbakestill til standard",
"it": "Ripristina valori predefiniti",
@@ -648,7 +648,7 @@
"en": "All information will be deleted",
"ja": "すべての情報が削除されます",
"zh-CN": "确定要重置所有设置吗?",
"zh-TW": "所有資訊將會被刪除",
"zh-TW": "確定要重置所有設定嗎?",
"ko-KR": "모든 설정을 재설정하시겠습니까?",
"no": "All informasjon vil bli slettet",
"it": "Tutte le informazioni verranno eliminate",
@@ -678,7 +678,7 @@
"en": "Changing workspace settings will end the current session",
"ja": "ワークスペース設定を変更すると、現在のセッションが終了します",
"zh-CN": "确定要结束当前会话吗?",
"zh-TW": "變更工作區設定將會結束目前工作階段",
"zh-TW": "確定要結束目前工作階段嗎?",
"ko-KR": "현재 세션을 종료하시겠습니까?",
"no": "Endring av arbeidsområdeinnstillinger vil avslutte gjeldende økt",
"it": "La modifica delle impostazioni dell'area di lavoro terminerà la sessione corrente",
@@ -738,7 +738,7 @@
"en": "GitHub Token",
"ja": "GitHubトークン",
"zh-CN": "GitHub令牌",
"zh-TW": "GitHub 權杖",
"zh-TW": "GitHub權杖",
"ko-KR": "GitHub 토큰",
"no": "GitHub-token",
"it": "Token GitHub",
@@ -753,7 +753,7 @@
"en": "GitHub Token (Optional)",
"ja": "GitHubトークン(任意)",
"zh-CN": "(可选)",
"zh-TW": "GitHub 權杖(選填)",
"zh-TW": "(選填)",
"ko-KR": "(선택사항)",
"no": "GitHub-token (valgfritt)",
"it": "Token GitHub (opzionale)",
@@ -798,7 +798,7 @@
"en": "Enable analytics",
"ja": "アナリティクスを有効にする",
"zh-CN": "启用分析",
"zh-TW": "啟用分析功能",
"zh-TW": "啟用分析",
"ko-KR": "분석 활성화",
"no": "Aktiver analyse",
"it": "Abilita analisi",
@@ -813,7 +813,7 @@
"en": "Invalid GitHub token",
"ja": "GitHubトークンが無効です",
"zh-CN": "GitHub令牌无效",
"zh-TW": "GitHub 權杖無效",
"zh-TW": "GitHub權杖無效",
"ko-KR": "GitHub 토큰이 유효하지 않습니다",
"no": "Ugyldig GitHub-token",
"it": "Token GitHub non valido",
@@ -843,7 +843,7 @@
"en": "Configure Github Repositories",
"ja": "GitHubリポジトリを設定",
"zh-CN": "配置GitHub仓库",
"zh-TW": "設定 GitHub 儲存庫",
"zh-TW": "設定GitHub儲存庫",
"ko-KR": "GitHub 저장소 설정",
"no": "Konfigurer GitHub-repositorier",
"it": "Configura repository GitHub",
@@ -858,7 +858,7 @@
"en": "Click here for instructions",
"ja": "手順はこちらをクリック",
"zh-CN": "点击查看说明",
"zh-TW": "點此檢視說明",
"zh-TW": "點擊查看說明",
"ko-KR": "설명을 보려면 클릭하세요",
"no": "Klikk her for instruksjoner",
"it": "Clicca qui per le istruzioni",
@@ -888,7 +888,7 @@
"en": "LLM Model",
"ja": "LLMモデル",
"zh-CN": "LLM模型",
"zh-TW": "LLM 模型",
"zh-TW": "LLM模型",
"ko-KR": "LLM 모델",
"no": "LLM-modell",
"it": "Modello LLM",
@@ -905,7 +905,7 @@
"de": "Standard: ./workspace",
"ko-KR": "워크스페이스 디렉토리 경로 입력",
"no": "Standard: ./workspace",
"zh-TW": "預設:./workspace",
"zh-TW": "輸入工作區目錄路徑",
"ar": "الافتراضي: ./workspace",
"fr": "Par défaut: ./workspace",
"it": "Predefinito: ./workspace",
@@ -950,7 +950,7 @@
"de": "Wähle ein Modell",
"ko-KR": "모델 선택",
"no": "Velg en modell",
"zh-TW": "選擇模型",
"zh-TW": "選擇模型",
"it": "Seleziona un modello",
"pt": "Selecione um modelo",
"es": "Seleccionar un modelo",
@@ -965,7 +965,7 @@
"de": "Agent",
"ko-KR": "에이전트",
"no": "Agent",
"zh-TW": "智慧代理",
"zh-TW": "代理",
"it": "Agente",
"pt": "Agente",
"es": "Agente",
@@ -980,7 +980,7 @@
"de": "Wähle einen Agenten",
"ko-KR": "에이전트 선택",
"no": "Velg en agent",
"zh-TW": "選擇智慧代理",
"zh-TW": "選擇代理",
"it": "Seleziona un agente",
"pt": "Selecione um agente",
"es": "Seleccionar un agente",
@@ -1010,7 +1010,7 @@
"de": "Wähle eine Sprache",
"ko-KR": "언어 선택",
"no": "Velg et språk",
"zh-TW": "選擇語言",
"zh-TW": "選擇語言",
"it": "Seleziona una lingua",
"pt": "Selecione um idioma",
"es": "Seleccionar un idioma",
@@ -1025,7 +1025,7 @@
"zh-CN": "安全性",
"ko-KR": "보안",
"no": "Sikkerhetsanalysator",
"zh-TW": "安全性分析工具",
"zh-TW": "安全性",
"it": "Analizzatore di sicurezza",
"pt": "Analisador de segurança",
"es": "Analizador de seguridad",
@@ -1040,7 +1040,7 @@
"zh-CN": "选择安全级别",
"ko-KR": "보안 수준 선택",
"no": "Velg en sikkerhetsanalysator (valgfritt)",
"zh-TW": "選擇安全性分析工具(選填)",
"zh-TW": "選擇安全等級",
"it": "Seleziona un analizzatore di sicurezza (opzionale)",
"pt": "Selecione um analisador de segurança (opcional)",
"es": "Seleccione un analizador de seguridad (opcional)",
@@ -1085,7 +1085,7 @@
"de": "Auf Standardwerte zurücksetzen",
"ko-KR": "재설정",
"no": "Tilbakestill til standardverdier",
"zh-TW": "還原為預設值",
"zh-TW": "重置",
"it": "Reimposta ai valori predefiniti",
"pt": "Redefinir para os padrões",
"es": "Restablecer valores predeterminados",
@@ -1205,7 +1205,7 @@
"zh-CN": "设置需要更新",
"ko-KR": "설정 업데이트 필요",
"no": "Vi har endret noen innstillinger i den siste oppdateringen. Ta deg tid til å se gjennom dem.",
"zh-TW": "我們在最新的更新中改變了一些設定,請花一些時間檢視這些更新",
"zh-TW": "設定需要更新",
"it": "Abbiamo modificato alcune impostazioni nell'ultimo aggiornamento. Prenditi un momento per rivederle.",
"pt": "Alteramos algumas configurações na última atualização. Reserve um momento para revisar.",
"es": "Hemos cambiado algunas configuraciones en la última actualización. Tómate un momento para revisarlas.",
@@ -1220,7 +1220,7 @@
"zh-CN": "代理加载中",
"ko-KR": "에이전트 로딩 중",
"no": "Vennligst vent mens agenten laster. Dette kan ta noen minutter...",
"zh-TW": "請稍待智慧代理載入,這可能需要幾分鐘的時間...",
"zh-TW": "代理載入中",
"it": "Attendere mentre l'agente si carica. Potrebbe richiedere alcuni minuti...",
"pt": "Por favor, aguarde enquanto o agente carrega. Isso pode levar alguns minutos...",
"es": "Por favor, espere mientras el agente se carga. Esto puede tardar unos minutos...",
@@ -1235,7 +1235,7 @@
"zh-CN": "代理运行中",
"ko-KR": "에이전트 실행 중",
"no": "Vennligst stopp agenten før du redigerer disse innstillingene.",
"zh-TW": "請先停止智慧代理再編輯這些設定。",
"zh-TW": "代理執行中",
"it": "Si prega di fermare l'agente prima di modificare queste impostazioni.",
"pt": "Por favor, pare o agente antes de editar estas configurações.",
"es": "Por favor, detenga el agente antes de editar estas configuraciones.",
@@ -1248,7 +1248,7 @@
"en": "Failed to fetch models and agents",
"zh-CN": "获取模型时出错",
"de": "Fehler beim Abrufen der Modelle und Agenten",
"zh-TW": "取得模型與智慧代理失敗",
"zh-TW": "取得模型時出錯",
"es": "Error al obtener modelos y agentes",
"fr": "Échec de la récupération des modèles et des agents",
"it": "Impossibile recuperare modelli e agenti",
@@ -1261,13 +1261,11 @@
},
"CONFIGURATION$SETTINGS_NOT_FOUND": {
"en": "Settings not found. Please check your API key",
"es": "Configuraciones no encontradas. Por favor revisa tu API key",
"zh-TW": "找不到設定。請檢查您的 API 金鑰"
"es": "Configuraciones no encontradas. Por favor revisa tu API key"
},
"CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE": {
"en": "terms of service",
"es": "términos de servicio",
"zh-TW": "服務條款"
"es": "términos de servicio"
},
"SESSION$SERVER_CONNECTED_MESSAGE": {
"en": "Connected to server",
@@ -1287,7 +1285,7 @@
"en": "Error handling message",
"zh-CN": "处理会话时出错",
"de": "Fehler beim Verarbeiten der Nachricht",
"zh-TW": "處理工作階段時發生錯誤",
"zh-TW": "處理工作階段時出錯",
"es": "Error al procesar el mensaje",
"fr": "Erreur lors du traitement du message",
"it": "Errore durante l'elaborazione del messaggio",
@@ -1302,7 +1300,7 @@
"en": "Error connecting to session",
"zh-CN": "连接会话时出错",
"de": "Verbindung zur Sitzung fehlgeschlagen",
"zh-TW": "連線工作階段時發生錯誤",
"zh-TW": "連線工作階段時出錯",
"es": "Error al conectar con la sesión",
"fr": "Erreur de connexion à la session",
"it": "Errore durante la connessione alla sessione",
@@ -1332,7 +1330,7 @@
"en": "Error uploading file",
"zh-CN": "上传时出错",
"de": "Fehler beim Hochladen der Datei",
"zh-TW": "上傳時發生錯誤",
"zh-TW": "上傳時出錯",
"es": "Error al subir el archivo",
"fr": "Erreur lors du téléchargement du fichier",
"it": "Errore durante il caricamento del file",
@@ -1407,7 +1405,7 @@
"en": "Error refreshing workspace",
"zh-CN": "刷新时出错",
"de": "Fehler beim Aktualisieren des Arbeitsbereichs",
"zh-TW": "重新整理時發生錯誤",
"zh-TW": "重新整理時出錯",
"es": "Error al actualizar el espacio de trabajo",
"fr": "Erreur lors de l'actualisation de l'espace de travail",
"it": "Errore durante l'aggiornamento dell'area di lavoro",
@@ -1481,7 +1479,7 @@
"EXPLORER$VSCODE_SWITCHING_MESSAGE": {
"en": "Switching to VS Code in 3 seconds...\nImportant: Please inform the agent of any changes you make in VS Code. To avoid conflicts, wait for the assistant to complete its work before making your own changes.",
"zh-CN": "切换到VS Code中...",
"zh-TW": "切換到 VS Code 中...",
"zh-TW": "切換到VS Code中...",
"ja": "3秒後にVS Codeに切り替わります...\n重要:VS Codeで行った変更はエージェントに通知してください。競合を避けるため、アシスタントの作業が完了するまで自身の変更を待ってください。",
"ko-KR": "VS Code로 전환 중...",
"no": "Bytter til VS Code om 3 sekunder...\nViktig: Vennligst informer agenten om eventuelle endringer du gjør i VS Code. For å unngå konflikter, vent til assistenten er ferdig med sitt arbeid før du gjør dine egne endringer.",
@@ -1496,7 +1494,7 @@
"EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE": {
"en": "Error switching to VS Code: {{error}}",
"zh-CN": "切换到VS Code时出错",
"zh-TW": "切換到 VS Code 時發生錯誤",
"zh-TW": "切換到VS Code時出錯",
"ja": "VS Codeへの切り替え中にエラーが発生しました: {{error}}",
"ko-KR": "VS Code로 전환 중 오류 발생",
"no": "Feil ved bytte til VS Code: {{error}}",
@@ -1512,7 +1510,7 @@
"en": "Return to existing session?",
"de": "Zurück zu vorhandener Sitzung?",
"zh-CN": "是否继续未完成的会话?",
"zh-TW": "是否繼續未完成的會話",
"zh-TW": "是否繼續未完成的會話?",
"es": "¿Volver a la sesión existente?",
"fr": "Revenir à la session existante ?",
"it": "Tornare alla sessione esistente?",
@@ -1572,7 +1570,7 @@
"en": "Share feedback",
"de": "Feedback teilen",
"zh-CN": "分享反馈",
"zh-TW": "分享饋",
"zh-TW": "分享饋",
"es": "Compartir comentarios",
"fr": "Partager des commentaires",
"it": "Condividi feedback",
@@ -1587,7 +1585,7 @@
"en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data.",
"de": "Um uns zu verbessern, sammeln wir Feedback aus Ihren Interaktionen, um unsere Prompts zu verbessern. Durch das Absenden dieses Formulars stimmen Sie der Erfassung dieser Daten zu.",
"zh-CN": "为了帮助我们改进,我们会收集您的互动反馈以改进我们的提示。提交此表单即表示您同意我们收集这些数据。",
"zh-TW": "為了幫助我們改進,我們會收集您的互動饋以改進我們的提示。提交此表單即表示您同意我們收集這些資料。",
"zh-TW": "為了幫助我們改進,我們會收集您的互動饋以改進我們的提示。提交此表單即表示您同意我們收集這些數據。",
"es": "Para ayudarnos a mejorar, recopilamos comentarios de sus interacciones para mejorar nuestras indicaciones. Al enviar este formulario, usted consiente que recopilemos estos datos.",
"fr": "Pour nous aider à nous améliorer, nous recueillons des commentaires de vos interactions pour améliorer nos invites. En soumettant ce formulaire, vous consentez à ce que nous collections ces données.",
"it": "Per aiutarci a migliorare, raccogliamo feedback dalle tue interazioni per migliorare i nostri prompt. Inviando questo modulo, acconsenti alla raccolta di questi dati.",
@@ -1617,7 +1615,7 @@
"en": "Contribute to public dataset",
"de": "Zum öffentlichen Datensatz beitragen",
"zh-CN": "贡献到公共数据集",
"zh-TW": "貢獻到公共資料集",
"zh-TW": "貢獻到公共數據集",
"es": "Contribuir al conjunto de datos público",
"fr": "Contribuer à l'ensemble de données public",
"it": "Contribuisci al dataset pubblico",
@@ -1677,7 +1675,7 @@
"en": "Password copied to clipboard.",
"es": "Contraseña copiada al portapapeles.",
"zh-CN": "密码已复制到剪贴板。",
"zh-TW": "密碼已複製到剪貼簿。",
"zh-TW": "密碼已複製到剪貼。",
"ko-KR": "비밀번호가 클립보드에 복사되었습니다.",
"no": "Passord kopiert til utklippstavlen.",
"ar": "تم نسخ كلمة المرور إلى الحافظة.",
@@ -1692,7 +1690,7 @@
"en": "Go to shared feedback",
"es": "Ir a feedback compartido",
"zh-CN": "转到共享反馈",
"zh-TW": "前往分享回饋",
"zh-TW": "前往共享反饋",
"ko-KR": "공유된 피드백으로 이동",
"no": "Gå til delt tilbakemelding",
"ar": "الذهاب إلى التعليقات المشتركة",
@@ -1737,7 +1735,7 @@
"en": "Failed to share, please contact the developers:",
"es": "Error al compartir, por favor contacta con los desarrolladores:",
"zh-CN": "分享失败,请联系开发人员:",
"zh-TW": "分享失敗,請聯開發人員:",
"zh-TW": "分享失敗,請聯開發人員:",
"ko-KR": "공유 실패, 개발자에게 문의하세요:",
"no": "Deling mislyktes, vennligst kontakt utviklerne:",
"ar": "فشل المشاركة، يرجى الاتصال بالمطورين:",
@@ -1842,7 +1840,7 @@
"en": "Ask for user confirmation on risk severity:",
"es": "Preguntar por confirmación del usuario sobre severidad del riesgo:",
"zh-CN": "询问用户确认风险等级:",
"zh-TW": "詢問使用者確認風險等級:",
"zh-TW": "詢問用戶確認風險等級:",
"ko-KR": "위험 심각도에 대한 사용자 확인 요청:",
"no": "Be om brukerbekreftelse på risikoalvorlighet:",
"ar": "اطلب تأكيد المستخدم على مستوى الخطورة:",
@@ -1902,7 +1900,7 @@
"en": "Click to learn more",
"es": "Clic para aprender más",
"zh-CN": "点击了解更多",
"zh-TW": "點了解更多",
"zh-TW": "點了解更多",
"ko-KR": "자세히 알아보기",
"no": "Klikk for å lære mer",
"ar": "انقر لمعرفة المزيد",
@@ -2022,7 +2020,7 @@
"en": "Agent is initialized, waiting for task...",
"de": "Agent ist initialisiert und wartet auf Aufgabe...",
"zh-CN": "智能体已初始化,等待任务中...",
"zh-TW": "智慧代理已初始化,等待任務中...",
"zh-TW": "智能體已初始化,等待任務中...",
"ko-KR": "에이전트가 초기화되었습니다. 작업을 기다리는 중...",
"no": "Agenten er initialisert, venter på oppgave...",
"it": "L'agente è inizializzato, in attesa di compiti...",
@@ -2037,7 +2035,7 @@
"en": "Agent is running task",
"de": "Agent führt Aufgabe aus",
"zh-CN": "智能体正在执行任务...",
"zh-TW": "智慧代理正在執行任務...",
"zh-TW": "智能體正在執行任務...",
"ko-KR": "에이전트가 작업을 실행 중입니다",
"no": "Agenten utfører oppgave",
"it": "L'agente sta eseguendo il compito",
@@ -2052,7 +2050,7 @@
"en": "Agent is awaiting user input...",
"de": "Agent wartet auf Benutzereingabe...",
"zh-CN": "智能体正在等待用户输入...",
"zh-TW": "智慧代理正在等待使用者輸入...",
"zh-TW": "智能體正在等待用戶輸入...",
"ko-KR": "에이전트가 사용자 입력을 기다리고 있습니다...",
"no": "Agenten venter på brukerinndata...",
"it": "L'agente è in attesa dell'input dell'utente...",
@@ -2066,7 +2064,7 @@
"CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE": {
"en": "Agent is Rate Limited",
"zh-CN": "智能体已达到速率限制",
"zh-TW": "智慧代理已達到速率限制",
"zh-TW": "智能體已達到速率限制",
"de": "Agent ist ratenbegrenzt",
"ko-KR": "에이전트가 속도 제한되었습니다",
"no": "Agenten er hastighetsbegrenset",
@@ -2082,7 +2080,7 @@
"en": "Agent has paused.",
"de": "Agent pausiert.",
"zh-CN": "智能体已暂停",
"zh-TW": "智慧代理已暫停",
"zh-TW": "智能體已暫停",
"ko-KR": "에이전트가 일시 중지되었습니다.",
"no": "Agenten har pauset.",
"it": "L'agente ha messo in pausa.",
@@ -2097,7 +2095,7 @@
"en": "Let's start building!",
"ja": "開発を始めましょう!",
"zh-CN": "让我们开始构建!",
"zh-TW": "讓我們開始構!",
"zh-TW": "讓我們開始構",
"ko-KR": "시작해봅시다!",
"fr": "Commençons à construire !",
"es": "¡Empecemos a construir!",
@@ -2112,7 +2110,7 @@
"en": "OpenHands makes it easy to build and maintain software using a simple prompt.",
"ja": "OpenHandsは、シンプルなプロンプトを使用してソフトウェアの開発と保守を簡単にします。",
"zh-CN": "OpenHands 使用简单的提示来轻松构建和维护软件。",
"zh-TW": "OpenHands 使用簡單的提示來輕鬆構和維護軟。",
"zh-TW": "OpenHands 使用簡單的提示來輕鬆構和維護軟。",
"ko-KR": "OpenHands는 간단한 프롬프트를 사용하여 소프트웨어를 쉽게 구축하고 유지관리할 수 있게 해줍니다.",
"fr": "OpenHands facilite la construction et la maintenance de logiciels à l'aide d'une invite simple.",
"es": "OpenHands facilita la construcción y el mantenimiento de software usando un prompt simple.",
@@ -2157,7 +2155,7 @@
"en": "Create a Hello World app",
"ja": "Hello World アプリを作成",
"zh-CN": "创建一个 Hello World 应用",
"zh-TW": "建一個 Hello World 應用程式",
"zh-TW": "建一個 Hello World 應用",
"ko-KR": "Hello World 앱 만들기",
"fr": "Créer une application Hello World",
"es": "Crear una aplicación Hello World",
@@ -2172,7 +2170,7 @@
"en": "Build a todo list application",
"ja": "ToDoリストアプリを開発する",
"zh-CN": "构建一个待办事项列表应用",
"zh-TW": "構一個待辦事項列表應用程式",
"zh-TW": "構一個待辦事項列表應用",
"ko-KR": "할 일 목록 앱 만들기",
"fr": "Construire une application de liste de tâches",
"es": "Construir una aplicación de lista de tareas",
@@ -2187,7 +2185,7 @@
"en": "Write a bash script that shows the top story on Hacker News",
"ja": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
"zh-CN": "编写一个显示Hacker News头条新闻的bash脚本",
"zh-TW": "編寫一個顯示 Hacker News 頭條新聞的 bash 腳本",
"zh-TW": "編寫一個顯示Hacker News頭條新聞的bash腳本",
"ko-KR": "Hacker News의 상위 기사를 보여주는 bash 스크립트 작성하기",
"fr": "Écrire un script bash qui affiche l'article principal de Hacker News",
"es": "Escribir un script bash que muestre la noticia principal de Hacker News",
@@ -2217,7 +2215,7 @@
"en": "Connect to GitHub",
"ja": "GitHubに接続",
"zh-CN": "连接到GitHub",
"zh-TW": "連線到 GitHub",
"zh-TW": "連接到GitHub",
"ko-KR": "GitHub에 연결",
"fr": "Se connecter à GitHub",
"es": "Conectar con GitHub",
@@ -2247,7 +2245,7 @@
"en": "Add more repositories...",
"ja": "リポジトリを追加...",
"zh-CN": "添加更多仓库...",
"zh-TW": "新增更多儲存庫...",
"zh-TW": "添加更多儲存庫...",
"ko-KR": "더 많은 저장소 추가...",
"fr": "Ajouter plus de dépôts...",
"es": "Agregar más repositorios...",
@@ -2427,7 +2425,7 @@
"en": "Resume the agent task",
"ja": "エージェントのタスクを再開",
"zh-CN": "恢复代理任务",
"zh-TW": "恢復智慧代理任務",
"zh-TW": "恢復代理任務",
"ko-KR": "에이전트 작업 재개",
"fr": "Reprendre la tâche de l'agent",
"es": "Reanudar la tarea del agente",
@@ -2442,7 +2440,7 @@
"en": "Pause the current task",
"ja": "現在のタスクを一時停止",
"zh-CN": "暂停当前任务",
"zh-TW": "暫停前任務",
"zh-TW": "暫停前任務",
"ko-KR": "현재 작업 일시 중지",
"fr": "Mettre en pause la tâche actuelle",
"es": "Pausar la tarea actual",
@@ -2577,7 +2575,7 @@
"en": "Upload a .zip",
"ja": ".zipファイルをアップロード",
"zh-CN": "上传.zip文件",
"zh-TW": "上傳 .zip 檔案",
"zh-TW": "上傳.zip檔案",
"ko-KR": ".zip 파일 업로드",
"fr": "Télécharger un .zip",
"es": "Subir un .zip",
@@ -2637,7 +2635,7 @@
"en": "Auto-merge Dependabot PRs",
"ja": "DependabotのPRを自動マージ",
"zh-CN": "自动合并Dependabot PR",
"zh-TW": "自動合併 Dependabot PR",
"zh-TW": "自動合併Dependabot PR",
"ko-KR": "Dependabot PR 자동 병합",
"fr": "Fusion automatique des PR Dependabot",
"es": "Auto-fusionar PRs de Dependabot",
@@ -2652,7 +2650,7 @@
"en": "Agent has stopped.",
"de": "Agent hat angehalten.",
"zh-CN": "智能体已停止",
"zh-TW": "智慧代理已停止",
"zh-TW": "智能體已停止",
"ko-KR": "에이전트가 중지되었습니다.",
"no": "Agenten har stoppet.",
"it": "L'agente si è fermato.",
@@ -2667,7 +2665,7 @@
"en": "Agent has finished the task.",
"de": "Agent hat die Aufgabe erledigt.",
"zh-CN": "智能体已完成任务",
"zh-TW": "智慧代理已完成任務",
"zh-TW": "智能體已完成任務",
"ko-KR": "에이전트가 작업을 완료했습니다.",
"no": "Agenten har fullført oppgaven.",
"it": "L'agente ha completato il compito.",
@@ -2682,7 +2680,7 @@
"en": "Agent has rejected the task.",
"de": "Agent hat die Aufgabe abgelehnt.",
"zh-CN": "智能体拒绝任务",
"zh-TW": "智慧代理拒絕任務",
"zh-TW": "智能體拒絕任務",
"ko-KR": "에이전트가 작업을 거부했습니다.",
"no": "Agenten har avvist oppgaven.",
"it": "L'agente ha rifiutato il compito.",
@@ -2697,7 +2695,7 @@
"en": "Agent encountered an error.",
"de": "Agent ist auf einen Fehler gelaufen.",
"zh-CN": "智能体遇到错误",
"zh-TW": "智慧代理遇到錯誤",
"zh-TW": "智能體遇到錯誤",
"ko-KR": "에이전트에 오류가 발생했습니다.",
"no": "Agenten støtte på en feil.",
"it": "L'agente ha riscontrato un errore.",
@@ -2712,7 +2710,7 @@
"en": "Agent is awaiting user confirmation for the pending action.",
"de": "Agent wartet auf die Bestätigung des Benutzers für die ausstehende Aktion.",
"zh-CN": "代理正在等待用户确认待处理的操作。",
"zh-TW": "智慧代理正在等待用戶確認待處理的操作。",
"zh-TW": "代理正在等待用戶確認待處理的操作。",
"ko-KR": "에이전트가 대기 중인 작업에 대한 사용자 확인을 기다리고 있습니다.",
"no": "Agenten venter på brukerbekreftelse for den ventende handlingen.",
"it": "L'agente è in attesa della conferma dell'utente per l'azione in sospeso.",
@@ -2727,7 +2725,7 @@
"en": "Agent action has been confirmed!",
"de": "Die Aktion des Agenten wurde bestätigt!",
"zh-CN": "代理操作已确认!",
"zh-TW": "智慧代理操作已確認!",
"zh-TW": "代理操作已確認!",
"ko-KR": "에이전트 작업이 확인되었습니다!",
"no": "Agenthandlingen har blitt bekreftet!",
"it": "L'azione dell'agente è stata confermata!",
@@ -2742,7 +2740,7 @@
"en": "Agent action has been rejected!",
"de": "Die Aktion des Agenten wurde abgelehnt!",
"zh-CN": "代理操作已被拒绝!",
"zh-TW": "智慧代理操作已被拒絕!",
"zh-TW": "代理操作已被拒絕!",
"ko-KR": "에이전트 작업이 거부되었습니다!",
"no": "Agenthandlingen har blitt avvist!",
"it": "L'azione dell'agente è stata rifiutata!",
@@ -2759,7 +2757,7 @@
"de": "Sende eine Nachricht an den Assistenten...",
"ko-KR": "어시스턴트에게 메시지 보내기",
"no": "Send melding til assistenten...",
"zh-TW": "助理留言",
"zh-TW": "助理留言",
"it": "Invia un messaggio all'assistente...",
"pt": "Envie uma mensagem para o assistente...",
"es": "Mensaje al asistente...",
@@ -2834,7 +2832,7 @@
"de": "Senden",
"ko-KR": "전송",
"no": "Send",
"zh-TW": "送",
"zh-TW": "送",
"it": "Invia",
"pt": "Enviar",
"es": "Enviar",
@@ -2894,7 +2892,7 @@
"de": "Nachricht senden",
"ko-KR": "메시지 보내기",
"no": "Send melding",
"zh-TW": "送訊息",
"zh-TW": "送訊息",
"it": "Invia messaggio",
"pt": "Enviar mensagem",
"es": "Enviar mensaje",
@@ -2954,7 +2952,7 @@
"zh-CN": "回到底部",
"ko-KR": "맨 아래로",
"no": "Til bunnen",
"zh-TW": "回到底",
"zh-TW": "回到底",
"it": "In fondo",
"pt": "Para o fundo",
"es": "Ir al final",
@@ -3086,7 +3084,7 @@
"SETTINGS$AGENT_TOOLTIP": {
"en": "Select the agent to use.",
"zh-CN": "选择要使用的智能体",
"zh-TW": "選擇要使用的智慧代理。",
"zh-TW": "選擇要使用的智能體。",
"de": "Wähle den zu verwendenden Agenten.",
"ko-KR": "사용할 에이전트를 선택하세요.",
"no": "Velg agenten som skal brukes.",
@@ -3116,7 +3114,7 @@
"SETTINGS$DISABLED_RUNNING": {
"en": "Cannot be changed while the agent is running.",
"zh-CN": "在智能体运行时无法更改",
"zh-TW": "智慧代理正在執行時無法更改。",
"zh-TW": "智能體正在執行時無法更改。",
"de": "Kann bei laufender Aufgabe nicht geändert werden.",
"ko-KR": "에이전트가 실행 중일 때는 변경할 수 없습니다.",
"no": "Kan ikke endres mens agenten kjører.",
@@ -3176,7 +3174,7 @@
"SETTINGS$AGENT_SELECT_ENABLED": {
"en": "Enable Agent Selection - Advanced Users",
"zh-CN": "启用智能体选择 - 高级用户",
"zh-TW": "啟用智慧代理選擇 - 進階使用者",
"zh-TW": "啟用智能體選擇 - 進階使用者",
"de": "Agentenauswahl aktivieren - Fortgeschrittene Benutzer",
"ko-KR": "에이전트 선택 활성화 - 고급 사용자",
"no": "Aktiver agentvalg - Avanserte brukere",
@@ -3206,7 +3204,7 @@
"PLANNER$EMPTY_MESSAGE": {
"en": "No plan created.",
"zh-CN": "计划未创建",
"zh-TW": "未建任何計劃。",
"zh-TW": "未建任何計劃。",
"de": "Kein Plan erstellt.",
"ko-KR": "생성된 계획이 없습니다.",
"no": "Ingen plan opprettet.",
@@ -3251,7 +3249,7 @@
"STATUS$STARTING_RUNTIME": {
"en": "Starting Runtime...",
"zh-CN": "启动运行时...",
"zh-TW": "啟動行時...",
"zh-TW": "啟動行時...",
"de": "Laufzeitumgebung wird gestartet...",
"ko-KR": "런타임 시작 중...",
"no": "Starter kjøretidsmiljø...",
@@ -3357,7 +3355,7 @@
"en": "GitHub token is invalid. Please try again.",
"es": "El token de GitHub no es válido. Por favor, inténtelo de nuevo.",
"zh-CN": "GitHub令牌无效。请重试。",
"zh-TW": "GitHub 權杖無效",
"zh-TW": "GitHub權杖無效",
"ko-KR": "GitHub 토큰이 유효하지 않습니다",
"ja": "GitHubトークンが無効です",
"no": "GitHub-token er ugyldig. Vennligst prøv igjen.",
@@ -3507,7 +3505,7 @@
"en": "Base URL",
"es": "URL base",
"zh-CN": "基础URL",
"zh-TW": "基礎 URL",
"zh-TW": "基礎URL",
"ko-KR": "기본 URL",
"ja": "ベースURL",
"no": "Base URL",
@@ -3522,7 +3520,7 @@
"en": "API Key",
"es": "API Key",
"zh-CN": "API密钥",
"zh-TW": "API 金鑰",
"zh-TW": "API金鑰",
"ko-KR": "API 키",
"ja": "APIキー",
"no": "API-nøkkel",
@@ -3537,7 +3535,7 @@
"en": "Don't know your API key?",
"es": "¿No sabes tu API key?",
"zh-CN": "不知道您的API密钥?",
"zh-TW": "不知道您的 API 金鑰?",
"zh-TW": "不知道您的API金鑰?",
"ko-KR": "API 키를 모르시나요?",
"ja": "APIキーがわかりませんか?",
"no": "Kjenner du ikke API-nøkkelen din?",
@@ -3552,7 +3550,7 @@
"en": "Click here for instructions",
"es": "Clic aquí para instrucciones",
"zh-CN": "点击这里查看说明",
"zh-TW": "點此檢視說明",
"zh-TW": "點擊這裡查看說明",
"ko-KR": "설명을 보려면 여기를 클릭하세요",
"ja": "説明を見るにはここをクリック",
"no": "Klikk her for instruksjoner",
@@ -3567,7 +3565,7 @@
"en": "Agent",
"es": "Agente",
"zh-CN": "代理",
"zh-TW": "智慧代理",
"zh-TW": "代理",
"ko-KR": "에이전트",
"ja": "エージェント",
"no": "Agent",
@@ -3642,7 +3640,7 @@
"en": "Reset to defaults",
"es": "Reiniciar valores por defect",
"zh-CN": "重置为默认值",
"zh-TW": "還原為預設值",
"zh-TW": "重置為預設值",
"ko-KR": "기본값으로 재설정",
"ja": "デフォルトに戻す",
"no": "Tilbakestill til standardverdier",
@@ -3746,7 +3744,7 @@
"PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB": {
"en": "Connect to GitHub",
"zh-CN": "连接到GitHub",
"zh-TW": "連線到 GitHub",
"zh-TW": "連線到GitHub",
"ko-KR": "GitHub에 연결",
"ja": "GitHubに接続",
"no": "Koble til GitHub",
@@ -3792,7 +3790,7 @@
"en": "Error authenticating with the LLM provider. Please check your API key",
"es": "Error autenticando con el proveedor de LLM. Por favor revisa tu API key",
"zh-CN": "LLM认证错误",
"zh-TW": "LLM 認證錯誤",
"zh-TW": "LLM認證錯誤",
"ko-KR": "LLM 인증 오류",
"ja": "LLM認証エラー",
"no": "Feil ved autentisering med LLM-leverandøren. Vennligst sjekk API-nøkkelen din",
@@ -3819,8 +3817,7 @@
"tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin."
},
"STATUS$LLM_RETRY": {
"en": "Retrying LLM request",
"zh-TW": "重新嘗試 LLM 請求中"
"en": "Retrying LLM request"
},
"AGENT_ERROR$BAD_ACTION": {
"en": "Agent tried to execute a malformed action.",
@@ -3856,7 +3853,7 @@
"en": "Connect to GitHub",
"es": "Conectar a GitHub",
"zh-CN": "连接到GitHub",
"zh-TW": "連線到 GitHub",
"zh-TW": "連線到GitHub",
"ko-KR": "GitHub에 연결",
"ja": "GitHubに接続",
"no": "Koble til GitHub",
@@ -3871,7 +3868,7 @@
"en": "Push to GitHub",
"es": "Subir a GitHub",
"zh-CN": "推送到GitHub",
"zh-TW": "推送到 GitHub",
"zh-TW": "推送到GitHub",
"ko-KR": "GitHub에 푸시",
"ja": "GitHubにプッシュ",
"no": "Push til GitHub",
@@ -4050,7 +4047,7 @@
"ACTION_MESSAGE$RUN_IPYTHON": {
"en": "Running a Python command",
"zh-CN": "运行IPython",
"zh-TW": "執行 IPython",
"zh-TW": "執行IPython",
"ko-KR": "IPython 실행",
"ja": "IPythonを実行",
"no": "Kjører en Python-kommando",
@@ -4140,7 +4137,7 @@
"OBSERVATION_MESSAGE$RUN_IPYTHON": {
"en": "Ran a Python command",
"zh-CN": "运行IPython",
"zh-TW": "執行 IPython",
"zh-TW": "執行IPython",
"ko-KR": "IPython 실행",
"ja": "IPythonを実行",
"no": "Kjørte en Python-kommando",
@@ -4215,7 +4212,7 @@
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
"zh-CN": "显示详情",
"zh-TW": "顯示詳細資訊",
"zh-TW": "顯示詳",
"ko-KR": "상세 정보 표시",
"ja": "詳細を表示",
"no": "Vis detaljer",
@@ -4230,7 +4227,7 @@
"EXPANDABLE_MESSAGE$HIDE_DETAILS": {
"en": "Hide details",
"zh-CN": "隐藏详情",
"zh-TW": "隱藏詳細資訊",
"zh-TW": "隱藏詳",
"ko-KR": "상세 정보 숨기기",
"ja": "詳細を非表示",
"no": "Skjul detaljer",
@@ -4246,7 +4243,7 @@
"en": "AI Provider Configuration",
"ja": "AI プロバイダー設定",
"zh-CN": "AI 提供商配置",
"zh-TW": "AI 供應商設定",
"zh-TW": "AI 提供商配置",
"ko-KR": "AI 제공자 設정",
"no": "AI-leverandørkonfigurasjon",
"it": "Configurazione del provider AI",
@@ -4321,7 +4318,7 @@
"en": "Add best practices docs for contributors",
"ja": "コントリビューター向けのベストプラクティスドキュメントを追加",
"zh-CN": "为贡献者添加最佳实践文档",
"zh-TW": "為貢獻者新增最佳實踐文",
"zh-TW": "為貢獻者添加最佳實踐文",
"ko-KR": "기여자를 위한 모범 사례 문서 추가",
"no": "Legg til beste praksis dokumentasjon for bidragsytere",
"it": "Aggiungere documenti sulle migliori pratiche per i contributori",
@@ -4336,7 +4333,7 @@
"en": "Add/improve a Dockerfile",
"ja": "Dockerfileを追加/改善",
"zh-CN": "添加/改进 Dockerfile",
"zh-TW": "新增/改進 Dockerfile",
"zh-TW": "添加/改進 Dockerfile",
"ko-KR": "Dockerfile 추가/개선",
"no": "Legg til/forbedre en Dockerfile",
"it": "Aggiungere/migliorare un Dockerfile",
@@ -4366,7 +4363,7 @@
"en": "No page loaded.",
"ja": "ページが読み込まれていません。",
"zh-CN": "页面未加载",
"zh-TW": "未載任何頁面。",
"zh-TW": "未載任何頁面。",
"de": "Keine Seite geladen.",
"ko-KR": "페이지가 로드되지 않았습니다.",
"no": "Ingen side lastet.",
@@ -4457,7 +4454,7 @@
"en": "Select a GitHub project",
"ja": "GitHubプロジェクトを選択",
"zh-CN": "选择GitHub项目",
"zh-TW": "選擇 GitHub 專案",
"zh-TW": "選擇GitHub專案",
"ko-KR": "GitHub 프로젝트 선택",
"fr": "Sélectionner un projet GitHub",
"es": "Seleccionar un proyecto de GitHub",
@@ -4472,7 +4469,7 @@
"en": "Send",
"ja": "送信",
"zh-CN": "发送",
"zh-TW": "送",
"zh-TW": "送",
"ko-KR": "보내기",
"fr": "Envoyer",
"es": "Enviar",
@@ -4514,7 +4511,6 @@
"de": "Was möchten Sie erstellen?"
},
"SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL": {
"en": "Enable Memory Condenser",
"zh-TW": "啟用記憶體壓縮器"
"en": "Enable Memory Condenser"
}
}
@@ -90,7 +90,15 @@ class CodeActAgent(Agent):
self.pending_actions: deque[Action] = deque()
self.reset()
# Retrieve the enabled tools
self.mock_function_calling = False
if not self.llm.is_function_calling_active():
logger.info(
f'Function calling not enabled for model {self.llm.config.model}. '
'Mocking function calling via prompting.'
)
self.mock_function_calling = True
# Function calling mode
self.tools = codeact_function_calling.get_tools(
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
@@ -303,7 +311,10 @@ class CodeActAgent(Agent):
and len(obs.set_of_marks) > 0
and self.config.enable_som_visual_browsing
and self.llm.vision_is_active()
and self.llm.is_visual_browser_tool_supported()
and (
self.mock_function_calling
or self.llm.is_visual_browser_tool_active()
)
):
text += 'Image: Current webpage screenshot (Note that only visible portion of webpage is present in the screenshot. You may need to scroll to view the remaining portion of the web-page.)\n'
message = Message(
@@ -389,6 +400,8 @@ class CodeActAgent(Agent):
'messages': self.llm.format_messages_for_llm(messages),
}
params['tools'] = self.tools
if self.mock_function_calling:
params['mock_function_calling'] = True
response = self.llm.completion(**params)
actions = codeact_function_calling.response_to_actions(response)
for action in actions:
@@ -43,14 +43,6 @@ class LLMSummarizingCondenserConfig(BaseModel):
llm_config: LLMConfig = Field(
..., description='Configuration for the LLM to use for condensing.'
)
keep_first: int = Field(
default=1,
description='The number of initial events to condense.',
ge=0,
)
max_size: int = Field(
default=10, description='Maximum number of events to keep.', ge=1
)
class AmortizedForgettingCondenserConfig(BaseModel):
+13 -21
View File
@@ -11,32 +11,24 @@ from openhands.events.serialization import event_to_dict
from openhands.llm.metrics import Metrics
class OpenHandsJSONEncoder(json.JSONEncoder):
def my_default_encoder(obj):
"""Custom JSON encoder that handles datetime and event objects"""
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Event):
return event_to_dict(obj)
if isinstance(obj, Metrics):
return obj.get()
if isinstance(obj, ModelResponse):
return obj.model_dump()
if isinstance(obj, CmdOutputMetadata):
return obj.model_dump()
return super().default(obj)
# Create a single reusable encoder instance
_json_encoder = OpenHandsJSONEncoder()
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Event):
return event_to_dict(obj)
if isinstance(obj, Metrics):
return obj.get()
if isinstance(obj, ModelResponse):
return obj.model_dump()
if isinstance(obj, CmdOutputMetadata):
return obj.model_dump()
return json.JSONEncoder().default(obj)
def dumps(obj, **kwargs):
"""Serialize an object to str format"""
if not kwargs:
return _json_encoder.encode(obj)
return json.dumps(obj, cls=OpenHandsJSONEncoder, **kwargs)
return json.dumps(obj, default=my_default_encoder, **kwargs)
def loads(json_str, **kwargs):
-1
View File
@@ -42,7 +42,6 @@ Reminder:
- Only call one function at a time
- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after.
- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
</IMPORTANT>
"""
STOP_WORDS = ['</function']
+8 -12
View File
@@ -197,7 +197,7 @@ class LLM(RetryMixin, DebugMixin):
from openhands.core.utils import json
messages: list[dict[str, Any]] | dict[str, Any] = []
mock_function_calling = not self.is_function_calling_active()
mock_function_calling = kwargs.pop('mock_function_calling', False)
# some callers might send the model and messages directly
# litellm allows positional args, like completion(model, messages, **kwargs)
@@ -216,21 +216,18 @@ class LLM(RetryMixin, DebugMixin):
# ensure we work with a list of messages
messages = messages if isinstance(messages, list) else [messages]
# handle conversion of to non-function calling messages if needed
original_fncall_messages = copy.deepcopy(messages)
mock_fncall_tools = None
# if the agent or caller has defined tools, and we mock via prompting, convert the messages
if mock_function_calling and 'tools' in kwargs:
if mock_function_calling:
assert (
'tools' in kwargs
), "'tools' must be in kwargs when mock_function_calling is True"
messages = convert_fncall_messages_to_non_fncall_messages(
messages, kwargs['tools']
)
kwargs['messages'] = messages
# add stop words if the model supports it
if self.config.model not in MODELS_WITHOUT_STOP_WORDS:
kwargs['stop'] = STOP_WORDS
mock_fncall_tools = kwargs.pop('tools')
# if we have no messages, something went very wrong
@@ -259,10 +256,9 @@ class LLM(RetryMixin, DebugMixin):
self.metrics.add_response_latency(latency, response_id)
non_fncall_response = copy.deepcopy(resp)
# if we mocked function calling, and we have tools, convert the response back to function calling format
if mock_function_calling and mock_fncall_tools is not None:
if mock_function_calling:
assert len(resp.choices) == 1
assert mock_fncall_tools is not None
non_fncall_response_message = resp.choices[0].message
fn_call_messages_with_response = (
convert_non_fncall_messages_to_fncall_messages(
@@ -492,7 +488,7 @@ class LLM(RetryMixin, DebugMixin):
"""
return self._function_calling_active
def is_visual_browser_tool_supported(self) -> bool:
def is_visual_browser_tool_active(self) -> bool:
return (
self.config.model in VISUAL_BROWSING_TOOL_SUPPORTED_MODELS
or self.config.model.split('/')[-1] in VISUAL_BROWSING_TOOL_SUPPORTED_MODELS
@@ -1,112 +1,55 @@
from __future__ import annotations
from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.event import Event
from openhands.events.observation.agent import AgentCondensationObservation
from openhands.llm import LLM
from openhands.memory.condenser.condenser import RollingCondenser
from openhands.memory.condenser.condenser import Condenser
class LLMSummarizingCondenser(RollingCondenser):
"""A condenser that summarizes forgotten events.
class LLMSummarizingCondenser(Condenser):
"""A condenser that relies on a language model to summarize the event sequence as a single event."""
Maintains a condensed history and forgets old events when it grows too large,
keeping a special summarization event after the prefix that summarizes all previous summarizations
and newly forgotten events.
"""
def __init__(self, llm: LLM, max_size: int = 100, keep_first: int = 1):
if keep_first >= max_size // 2:
raise ValueError(
f'keep_first ({keep_first}) must be less than half of max_size ({max_size})'
)
if keep_first < 0:
raise ValueError(f'keep_first ({keep_first}) cannot be negative')
if max_size < 1:
raise ValueError(f'max_size ({max_size}) cannot be non-positive')
self.max_size = max_size
self.keep_first = keep_first
def __init__(self, llm: LLM):
self.llm = llm
super().__init__()
def condense(self, events: list[Event]) -> list[Event]:
"""Apply the amortized forgetting strategy with LLM summarization to the given list of events."""
if len(events) <= self.max_size:
return events
"""Applies an LLM to summarize the list of events.
head = events[: self.keep_first]
Raises:
Exception: If the LLM is unable to summarize the event sequence.
"""
try:
# Convert events to a format suitable for summarization
events_text = '\n'.join(f'{e.timestamp}: {e.message}' for e in events)
summarize_prompt = f'Please summarize these events:\n{events_text}'
target_size = self.max_size // 2
events_from_tail = target_size - len(head)
tail = events[-events_from_tail:]
resp = self.llm.completion(
messages=[{'content': summarize_prompt, 'role': 'user'}]
)
summary_response = resp.choices[0].message.content
summary_event = (
events[self.keep_first]
if isinstance(events[self.keep_first], AgentCondensationObservation)
else AgentCondensationObservation('No events summarized')
)
# Create a new summary event with the condensed content
summary_event = AgentCondensationObservation(summary_response)
# Identify events to be forgotten (those not in head or tail)
forgotten_events = []
for event in events[self.keep_first : -events_from_tail]:
if not isinstance(event, AgentCondensationObservation):
forgotten_events.append(event)
# Add metrics to state
self.add_metadata('response', resp.model_dump())
self.add_metadata('metrics', self.llm.metrics.get())
# Construct prompt for summarization
prompt = """You are maintaining state history for an LLM-based code agent. Track:
return [summary_event]
STATE: {File paths, function signatures, data structures}
TESTS: {Failing cases, error messages, outputs}
CHANGES: {Code edits, variable updates}
DEPS: {Dependencies, imports, external calls}
INTENT: {Why changes were made, acceptance criteria}
SKIP: {Git clones, build logs}
SUMMARIZE: {File listings}
MAX_LENGTH: Keep summaries under 1000 words
Example history format:
STATE: mod_float() in card.py updated
TESTS: test_format() passed
CHANGES: str(val) replaces f"{val:.16G}"
DEPS: None modified
INTENT: Fix float precision overflow"""
prompt + '\n\n'
prompt += ('\n' + summary_event.message + '\n') if summary_event.message else ''
prompt + '\n\n'
for forgotten_event in forgotten_events:
prompt += str(forgotten_event) + '\n\n'
response = self.llm.completion(
messages=[
{
'content': prompt,
'role': 'user',
},
],
)
summary = response.choices[0].message.content
self.add_metadata('response', response.model_dump())
self.add_metadata('metrics', self.llm.metrics.get())
return head + [AgentCondensationObservation(summary)] + tail
except Exception as e:
logger.error(f'Error condensing events: {str(e)}')
raise e
@classmethod
def from_config(
cls, config: LLMSummarizingCondenserConfig
) -> LLMSummarizingCondenser:
return LLMSummarizingCondenser(
llm=LLM(config=config.llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
)
return LLMSummarizingCondenser(llm=LLM(config=config.llm_config))
LLMSummarizingCondenser.register_config(LLMSummarizingCondenserConfig)
+1 -32
View File
@@ -133,14 +133,6 @@ class Runtime(FileEditRuntimeMixin):
if self.config.sandbox.runtime_startup_env_vars:
self.add_env_vars(self.config.sandbox.runtime_startup_env_vars)
def attach_github_token(self, token) -> None:
print('attaching token via runtime')
cmd = f'export GITHUB_TOKEN={json.dumps(token)};'
obs = self.run(CmdRunAction(cmd))
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
raise RuntimeError(f'Failed to update gh token: {obs.content}')
def close(self) -> None:
"""
This should only be called by conversation manager or closing the session.
@@ -180,14 +172,11 @@ class Runtime(FileEditRuntimeMixin):
# Note: we don't log the vars values, they're leaking info
logger.debug('Added env vars to IPython')
# Add env vars to the Bash shell and .bashrc for persistence
# Add env vars to the Bash shell
cmd = ''
bashrc_cmd = ''
for key, value in env_vars.items():
# Note: json.dumps gives us nice escaping for free
cmd += f'export {key}={json.dumps(value)}; '
# Add to .bashrc if not already present
bashrc_cmd += f'grep -q "^export {key}=" ~/.bashrc || echo "export {key}={json.dumps(value)}" >> ~/.bashrc; '
if not cmd:
return
cmd = cmd.strip()
@@ -201,15 +190,6 @@ class Runtime(FileEditRuntimeMixin):
f'Failed to add env vars [{env_vars.keys()}] to environment: {obs.content}'
)
# Add to .bashrc for persistence
bashrc_cmd = bashrc_cmd.strip()
logger.debug(f'Adding env var to .bashrc: {env_vars.keys()}')
obs = self.run(CmdRunAction(bashrc_cmd))
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
raise RuntimeError(
f'Failed to add env vars [{env_vars.keys()}] to .bashrc: {obs.content}'
)
def on_event(self, event: Event) -> None:
if isinstance(event, Action):
asyncio.get_event_loop().run_until_complete(self._handle_action(event))
@@ -220,17 +200,6 @@ class Runtime(FileEditRuntimeMixin):
event.set_hard_timeout(self.config.sandbox.timeout, blocking=False)
assert event.timeout is not None
try:
if isinstance(event, CmdRunAction):
print('found event action', event.command)
if '$GITHUB_TOKEN' in event.command:
print('token required by action', event.command)
await call_sync_from_async(
self.run,
CmdRunAction(
"export UNTESTED_GITHUB_TOKEN='this is a dummy token'"
),
)
observation: Observation = await call_sync_from_async(
self.run_action, event
)
@@ -401,32 +401,6 @@ class DockerRuntime(ActionExecutionClient):
return hosts
def pause(self):
"""Pause the runtime by stopping the container.
This is different from container.stop() as it ensures environment variables are properly preserved."""
if not self.container:
raise RuntimeError("Container not initialized")
# First, ensure all environment variables are properly persisted in .bashrc
# This is already handled by add_env_vars in base.py
# Stop the container
self.container.stop()
self.log('debug', f'Container {self.container_name} paused')
def resume(self):
"""Resume the runtime by starting the container.
This is different from container.start() as it ensures environment variables are properly restored."""
if not self.container:
raise RuntimeError("Container not initialized")
# Start the container
self.container.start()
self.log('debug', f'Container {self.container_name} resumed')
# Wait for the container to be ready
self._wait_until_alive()
@classmethod
async def delete(cls, conversation_id: str):
docker_client = cls._init_docker_client()
-21
View File
@@ -1,21 +0,0 @@
from dotenv import load_dotenv
from openhands.core.config import load_app_config
from openhands.server.config.server_config import load_server_config
from openhands.storage import get_file_store
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.settings.settings_store import SettingsStore
from openhands.utils.import_utils import get_impl
load_dotenv()
config = load_app_config()
server_config = load_server_config()
file_store = get_file_store(config.file_store, config.file_store_path)
ConversationStoreImpl = get_impl(
ConversationStore, # type: ignore
server_config.conversation_store_class,
)
SettingsStoreImpl = get_impl(SettingsStore, server_config.settings_store_class) # type: ignore
@@ -77,10 +77,6 @@ class ConversationManager(ABC):
async def send_to_event_stream(self, connection_id: str, data: dict):
"""Send data to an event stream."""
@abstractmethod
def update_token(self, connection_id: str):
"""Update/refresh the runtime gh token"""
@abstractmethod
async def disconnect_from_session(self, connection_id: str):
"""Disconnect from a session."""
@@ -10,8 +10,6 @@ from openhands.core.exceptions import AgentRuntimeUnavailableError
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema.agent import AgentState
from openhands.events.action import MessageAction
from openhands.events.action.commands import CmdRunAction
from openhands.events.event import EventSource
from openhands.events.stream import EventStream, session_exists
from openhands.server.session.conversation import Conversation
from openhands.server.session.session import ROOM_KEY, Session
@@ -89,17 +87,13 @@ class StandaloneConversationManager(ConversationManager):
return c
async def join_conversation(
self,
sid: str,
connection_id: str,
settings: Settings | None,
user_id: str | None,
self, sid: str, connection_id: str, settings: Settings, user_id: str | None
):
logger.info(f'join_conversation:{sid}:{connection_id}')
await self.sio.enter_room(connection_id, ROOM_KEY.format(sid=sid))
self._local_connection_id_to_session_id[connection_id] = sid
event_stream = await self._get_event_stream(sid)
if not event_stream and settings:
if not event_stream:
return await self.maybe_start_agent_loop(sid, settings, user_id)
return event_stream
@@ -155,8 +149,8 @@ class StandaloneConversationManager(ConversationManager):
self._close_session(sid) for sid in self._local_agent_loops_by_sid
)
return
except Exception:
logger.error('error_cleaning_stale')
except Exception as e:
logger.error(f'error_cleaning_stale')
await asyncio.sleep(_CLEANUP_INTERVAL)
async def get_running_agent_loops(
@@ -245,30 +239,6 @@ class StandaloneConversationManager(ConversationManager):
raise RuntimeError(f'no_connected_session:{connection_id}:{sid}')
async def update_token(self, connection_id: str):
await asyncio.sleep(1)
# print('updating token')
# session = self._local_agent_loops_by_sid.get(connection_id)
# print(f'found session for sid: {connection_id}')
# if session:
# try:
# await session.update_token()
# except Exception as e:
# print(f'error updating token: {str(e)}')
try:
event_stream = await self.join_conversation(
sid=connection_id,
connection_id=connection_id,
settings=None,
user_id=None,
)
cmd = 'export GITHUB_TOKEN="this is a dummy token";'
action = CmdRunAction(cmd, hidden=True)
event_stream.add_event(action, EventSource.ENVIRONMENT)
except Exception as e:
print(f'error updating token: {str(e)}')
async def disconnect_from_session(self, connection_id: str):
sid = self._local_connection_id_to_session_id.pop(connection_id, None)
logger.info(f'disconnect_from_session:{connection_id}:{sid}')
+1 -1
View File
@@ -3,7 +3,7 @@ import re
from openhands.core.config import AppConfig
from openhands.core.logger import openhands_logger as logger
from openhands.server.config_init import config as shared_config
from openhands.server.shared import config as shared_config
FILES_TO_IGNORE = ['.git/', '.DS_Store', 'node_modules/', '__pycache__/', 'lost+found/']
+3 -2
View File
@@ -14,13 +14,14 @@ from openhands.events.observation import (
from openhands.events.observation.agent import AgentStateChangedObservation
from openhands.events.serialization import event_to_dict
from openhands.events.stream import AsyncEventStreamWrapper
from openhands.server.config_init import (
from openhands.server.shared import (
ConversationStoreImpl,
SettingsStoreImpl,
config,
conversation_manager,
server_config,
sio,
)
from openhands.server.shared import conversation_manager, sio
from openhands.server.types import AppMode
+1 -2
View File
@@ -13,7 +13,6 @@ from starlette.types import ASGIApp
from openhands.server import shared
from openhands.server.auth import get_user_id
from openhands.server.config_init import SettingsStoreImpl
from openhands.server.types import SessionMiddlewareInterface
@@ -189,7 +188,7 @@ class GitHubTokenMiddleware(SessionMiddlewareInterface):
self.app = app
async def __call__(self, request: Request, call_next: Callable):
settings_store = await SettingsStoreImpl.get_instance(
settings_store = await shared.SettingsStoreImpl.get_instance(
shared.config, get_user_id(request)
)
settings = await settings_store.load()
@@ -11,14 +11,14 @@ from openhands.events.action.message import MessageAction
from openhands.events.stream import EventStreamSubscriber
from openhands.runtime import get_runtime_cls
from openhands.server.auth import get_user_id
from openhands.server.config_init import (
from openhands.server.routes.github import GithubServiceImpl
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import (
ConversationStoreImpl,
SettingsStoreImpl,
config,
conversation_manager,
)
from openhands.server.routes.github import GithubServiceImpl
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import conversation_manager
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.conversation_info import ConversationInfo
from openhands.storage.data_models.conversation_info_result_set import (
-15
View File
@@ -1,10 +1,8 @@
import warnings
import requests
from fastapi.responses import JSONResponse
from openhands.security.options import SecurityAnalyzers
from openhands.server.shared import conversation_manager
with warnings.catch_warnings():
warnings.simplefilter('ignore')
@@ -12,7 +10,6 @@ with warnings.catch_warnings():
from fastapi import (
APIRouter,
BackgroundTasks,
)
from openhands.controller.agent import Agent
@@ -116,15 +113,3 @@ async def get_config():
"""
return server_config.get_config()
@app.post('/refresh-runtime')
async def refresh_gh_token_in_runtime(
connection_id: str,
background_tasks: BackgroundTasks,
):
try:
background_tasks.add_task(conversation_manager.update_token, connection_id)
return JSONResponse(status_code=200, content={'message': 'updating'})
except Exception:
return JSONResponse(status_code=400, content={'message': 'updating'})
+1 -1
View File
@@ -3,9 +3,9 @@ from fastapi.responses import JSONResponse
from openhands.core.logger import openhands_logger as logger
from openhands.server.auth import get_user_id
from openhands.server.config_init import SettingsStoreImpl, config
from openhands.server.services.github_service import GitHubService
from openhands.server.settings import GETSettingsModel, POSTSettingsModel, Settings
from openhands.server.shared import SettingsStoreImpl, config
app = APIRouter(prefix='/api')
+5 -14
View File
@@ -4,13 +4,9 @@ import httpx
from fastapi import Request
from openhands.server.auth import get_github_token
from openhands.server.config_init import config, server_config
from openhands.server.data_models.gh_types import GitHubRepository, GitHubUser
from openhands.server.shared import SettingsStoreImpl, config, server_config
from openhands.server.types import AppMode, GhAuthenticationError, GHUnknownException
from openhands.storage.settings.settings_store import SettingsStore
from openhands.utils.import_utils import get_impl
SettingsStoreImpl = get_impl(SettingsStore, server_config.settings_store_class) # type: ignore
class GitHubService:
@@ -20,20 +16,15 @@ class GitHubService:
def __init__(self, user_id: str | None):
self.user_id = user_id
async def get_user_token(self) -> str:
settings_store = await SettingsStoreImpl.get_instance(config, self.user_id)
settings = await settings_store.load()
if settings and settings.github_token:
return settings.github_token.get_secret_value()
return ''
async def _get_github_headers(self):
"""
Retrieve the GH Token from settings store to construct the headers
"""
self.token = await self.get_user_token()
settings_store = await SettingsStoreImpl.get_instance(config, self.user_id)
settings = await settings_store.load()
if settings and settings.github_token:
self.token = settings.github_token.get_secret_value()
return {
'Authorization': f'Bearer {self.token}',
+1 -15
View File
@@ -196,14 +196,10 @@ class AgentSession:
env_vars = (
{
'GITHUB_TOKEN': github_token,
'SESSION_ID': self.sid, # Ensure Session ID is always set
}
if github_token
else {
'SESSION_ID': self.sid, # Ensure Session ID is always set
}
else None
)
self.runtime = runtime_cls(
config=config,
event_stream=self.event_stream,
@@ -333,13 +329,3 @@ class AgentSession:
# If 5 minutes have elapsed and we still don't have a controller, something has gone wrong
return AgentState.ERROR
return None
def update_token(self, token):
print('agent session updating token')
if self.runtime:
self.runtime.attach_github_token(token)
self.event_stream.set_secrets(
{
'github_token': token,
}
)
+3 -20
View File
@@ -6,9 +6,7 @@ import socketio
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig
from openhands.core.config.condenser_config import (
LLMSummarizingCondenserConfig,
)
from openhands.core.config.condenser_config import AmortizedForgettingCondenserConfig
from openhands.core.const.guide_url import TROUBLESHOOTING_URL
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema import AgentState
@@ -23,21 +21,14 @@ from openhands.events.observation.error import ErrorObservation
from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.events.stream import EventStreamSubscriber
from openhands.llm.llm import LLM
from openhands.server.config.server_config import load_server_config
from openhands.server.services.github_service import GitHubService
from openhands.server.session.agent_session import AgentSession
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.settings import Settings
from openhands.storage.files import FileStore
from openhands.utils.import_utils import get_impl
ROOM_KEY = 'room:{sid}'
server_config = load_server_config()
GithubImpl = get_impl(GitHubService, server_config.github_service_class)
class Session:
sid: str
sio: socketio.AsyncServer | None
@@ -117,8 +108,8 @@ class Session:
agent_config = self.config.get_agent_config(agent_cls)
if settings.enable_default_condenser:
default_condenser_config = LLMSummarizingCondenserConfig(
llm_config=llm.config, keep_first=3, max_size=40
default_condenser_config = AmortizedForgettingCondenserConfig(
keep_first=3, max_size=20
)
logger.info(f'Enabling default condenser: {default_condenser_config}')
agent_config.condenser = default_condenser_config
@@ -222,14 +213,6 @@ class Session:
return
await self._send(data)
async def update_token(self):
print('updating token in sessions')
gh_client = GithubImpl(self.user_id)
token = await gh_client.get_user_token()
if token:
print(f'retrieved user token {token[0:5]}')
self.agent_session.update_token('this is a dummy test token')
async def _send(self, data: dict[str, object]) -> bool:
try:
if not self.is_alive:
+19 -1
View File
@@ -1,13 +1,24 @@
import os
import socketio
from dotenv import load_dotenv
from openhands.server.config_init import config, file_store, server_config
from openhands.core.config import load_app_config
from openhands.server.config.server_config import load_server_config
from openhands.server.conversation_manager.conversation_manager import (
ConversationManager,
)
from openhands.storage import get_file_store
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.settings.settings_store import SettingsStore
from openhands.utils.import_utils import get_impl
load_dotenv()
config = load_app_config()
server_config = load_server_config()
file_store = get_file_store(config.file_store, config.file_store_path)
client_manager = None
redis_host = os.environ.get('REDIS_HOST')
if redis_host:
@@ -26,3 +37,10 @@ ConversationManagerImpl = get_impl(
server_config.conversation_manager_class,
)
conversation_manager = ConversationManagerImpl.get_instance(sio, config, file_store)
SettingsStoreImpl = get_impl(SettingsStore, server_config.settings_store_class) # type: ignore
ConversationStoreImpl = get_impl(
ConversationStore, # type: ignore
server_config.conversation_store_class,
)
-32
View File
@@ -81,35 +81,3 @@ def test_env_vars_added_by_config(temp_dir, runtime_cls):
and obs.content.strip().split('\r\n')[0].strip() == 'added_value'
)
_close_test_runtime(runtime)
def test_docker_runtime_env_vars_persist_after_restart(temp_dir):
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
runtime = _load_runtime(temp_dir, DockerRuntime)
# Add a test environment variable
runtime.add_env_vars({'GITHUB_TOKEN': 'test_token'})
# Verify the variable is set in current session
obs = runtime.run_action(CmdRunAction(command='echo $GITHUB_TOKEN'))
assert obs.exit_code == 0
assert obs.content.strip().split('\r\n')[0].strip() == 'test_token'
# Verify the variable is added to .bashrc
obs = runtime.run_action(
CmdRunAction(command='grep "^export GITHUB_TOKEN=" ~/.bashrc')
)
assert obs.exit_code == 0
assert 'export GITHUB_TOKEN=' in obs.content
# Test pause/resume cycle
runtime.pause()
runtime.resume()
# Verify the variable persists after restart
obs = runtime.run_action(CmdRunAction(command='echo $GITHUB_TOKEN'))
assert obs.exit_code == 0
assert obs.content.strip().split('\r\n')[0].strip() == 'test_token'
_close_test_runtime(runtime)
+10
View File
@@ -464,6 +464,16 @@ def test_browser_tool():
assert 'description' in BrowserTool['function']['parameters']['properties']['code']
def test_mock_function_calling():
# Test mock function calling when LLM doesn't support it
llm = Mock()
llm.is_function_calling_active = lambda: False
config = AgentConfig()
config.enable_prompt_extensions = False
agent = CodeActAgent(llm=llm, config=config)
assert agent.mock_function_calling is True
def test_response_to_actions_invalid_tool():
# Test response with invalid tool call
mock_response = Mock()
+40 -92
View File
@@ -15,7 +15,6 @@ from openhands.core.config.condenser_config import (
)
from openhands.core.config.llm_config import LLMConfig
from openhands.events.event import Event, EventSource
from openhands.events.observation.agent import AgentCondensationObservation
from openhands.events.observation.observation import Observation
from openhands.llm import LLM
from openhands.memory.condenser import Condenser
@@ -215,117 +214,47 @@ def test_recent_events_condenser():
assert result[2]._message == 'Event 5'
def test_llm_summarization_condenser_from_config():
"""Test that LLMSummarizingCondenser objects can be made from config."""
def test_llm_condenser_from_config():
"""Test that LLMCondensers can be made from config."""
config = LLMSummarizingCondenserConfig(
max_size=50,
keep_first=10,
llm_config=LLMConfig(
model='gpt-4o',
api_key='test_key',
),
)
)
condenser = Condenser.from_config(config)
assert isinstance(condenser, LLMSummarizingCondenser)
assert condenser.llm.config.model == 'gpt-4o'
assert condenser.llm.config.api_key.get_secret_value() == 'test_key'
assert condenser.max_size == 50
assert condenser.keep_first == 10
def test_llm_amortized_summarization_condenser_invalid_config():
"""Test that LLMSummarizingCondenser raises error when keep_first > max_size."""
pytest.raises(
ValueError,
LLMSummarizingCondenser,
llm=MagicMock(),
max_size=4,
keep_first=2,
)
pytest.raises(ValueError, LLMSummarizingCondenser, llm=MagicMock(), max_size=0)
pytest.raises(ValueError, LLMSummarizingCondenser, llm=MagicMock(), keep_first=-1)
def test_llm_condenser(mock_llm, mock_state):
"""Test that LLMCondensers use the LLM to generate a summary event."""
events = [
create_test_event('Event 1'),
create_test_event('Event 2'),
]
mock_state.history = events
def test_llm_summarizing_condenser_grows_to_max_size(mock_llm, mock_state):
"""Test that LLMSummarizingCondenser correctly maintains an event context up to max size."""
max_size = 15
condenser = LLMSummarizingCondenser(max_size=max_size, llm=mock_llm)
for i in range(max_size):
event = create_test_event(f'Event {i}')
mock_state.history.append(event)
results = condenser.condensed_history(mock_state)
assert len(results) == i + 1
def test_llm_summarizing_condenser_forgets_and_summarizes(mock_llm, mock_state):
"""Test that the LLMSummarizingCondenser forgets events and maintains a summary."""
max_size = 4
keep_first = 1
condenser = LLMSummarizingCondenser(
max_size=max_size, keep_first=keep_first, llm=mock_llm
)
# Add initial event
first_event = create_test_event('Event 0')
mock_state.history.append(first_event)
# Set up mock LLM response
mock_llm.set_mock_response_content('Summary of forgotten events')
# Add enough events to trigger forgetting
for i in range(max_size + 3): # +3 to ensure we're well past max_size
event = create_test_event(f'Event {i+1}')
mock_state.history.append(event)
# Get the condensed history
results = condenser.condensed_history(mock_state)
# We should have exactly 3 events:
# 1. First event (keep_first = 1)
# 2. Summary event
# 3. Most recent event
assert len(results) == 3, f'Expected 3 events, got {len(results)}: {results}'
assert (
results[0] == first_event
), f'First event should be {first_event}, got {results[0]}'
assert isinstance(
results[1], AgentCondensationObservation
), f'Second event should be a summary, got {results[1]}'
assert (
results[1].content == 'Summary of forgotten events'
), f"Summary content should be 'Summary of forgotten events', got {results[1].content}"
assert results[2] == event, f'Last event should be {event}, got {results[2]}'
def test_llm_summarizing_condenser_llm_call(mock_llm, mock_state):
"""Test that the LLM is called correctly when forgetting events."""
max_size = 4
keep_first = 1
condenser = LLMSummarizingCondenser(
max_size=max_size, keep_first=keep_first, llm=mock_llm
)
# Add initial event
first_event = create_test_event('Event 0')
mock_state.history.append(first_event)
# Set up mock LLM response
mock_llm.set_mock_response_content('Summary of forgotten events')
mock_llm.metrics = MagicMock()
mock_llm.metrics.get.return_value = {'test_metric': 1.0}
# Add enough events to trigger forgetting
for i in range(max_size):
event = create_test_event(f'Event {i+1}')
mock_state.history.append(event)
condenser.condensed_history(mock_state)
mock_llm.set_mock_response_content('Summary of events')
# Verify LLM was called with correct prompt
condenser = LLMSummarizingCondenser(llm=mock_llm)
result = condenser.condensed_history(mock_state)
assert len(result) == 1
assert result[0].content == 'Summary of events'
# Verify LLM was called with correct prompt.
mock_llm.completion.assert_called_once()
call_args = mock_llm.completion.call_args[1]
assert 'messages' in call_args
assert len(call_args['messages']) == 1
assert 'Event 1' in call_args['messages'][0]['content']
assert 'Event 2' in call_args['messages'][0]['content']
# Verify metrics were added to state
assert 'condenser_meta' in mock_state.extra_data
@@ -333,6 +262,25 @@ def test_llm_summarizing_condenser_llm_call(mock_llm, mock_state):
assert mock_state.extra_data['condenser_meta'][0]['metrics'] == {'test_metric': 1.0}
def test_llm_condenser_error():
"""Test that LLM errors are propagated during condensation."""
events = [create_test_event('Event 1', datetime(2024, 1, 1, 10, 0))]
mock_state = MagicMock()
mock_state.history = events
mock_llm = MagicMock()
mock_llm.completion.side_effect = Exception('LLM error')
condenser = LLMSummarizingCondenser(llm=mock_llm)
try:
condenser.condensed_history(mock_state)
raise AssertionError('Expected exception was not raised.')
except Exception as e:
assert str(e) == 'LLM error'
def test_amortized_forgetting_condenser_from_config():
"""Test that AmortizedForgettingCondenser objects can be made from config."""
max_size = 50
-56
View File
@@ -1,56 +0,0 @@
import gc
from datetime import datetime
import psutil
from openhands.core.utils.json import dumps
def get_memory_usage():
"""Get current memory usage of the process"""
process = psutil.Process()
return process.memory_info().rss
def test_json_encoder_memory_leak():
# Force garbage collection before test
gc.collect()
initial_memory = get_memory_usage()
# Create a large dataset that will need encoding
large_data = {
'datetime': datetime.now(),
'nested': [{'timestamp': datetime.now()} for _ in range(1000)],
}
# Track memory usage over multiple iterations
memory_samples = []
for i in range(10):
# Perform multiple serializations in each iteration
for _ in range(100):
dumps(large_data)
dumps(large_data, indent=2) # Test with kwargs too
# Force garbage collection
gc.collect()
memory_samples.append(get_memory_usage())
# Check if memory usage is stable (not continuously growing)
# We expect some fluctuation but not a steady increase
max_memory = max(memory_samples)
min_memory = min(memory_samples)
memory_variation = max_memory - min_memory
# Allow for some memory variation (2MB) due to Python's memory management
assert (
memory_variation < 2 * 1024 * 1024
), f'Memory usage unstable: {memory_variation} bytes variation'
# Also check total memory increase from start
final_memory = memory_samples[-1]
memory_increase = final_memory - initial_memory
# Allow for some memory increase (2MB) as some objects may be cached
assert (
memory_increase < 2 * 1024 * 1024
), f'Memory leak detected: {memory_increase} bytes increase'
+1 -1
View File
@@ -261,7 +261,7 @@ NON_FNCALL_MESSAGES = [
'content': [
{
'type': 'text',
'text': 'You are a helpful assistant that can interact with a computer to solve tasks.\n<IMPORTANT>\n* If user provides a path, you should NOT assume it\'s relative to the current working directory. Instead, you should explore the file system to find the file before working on it.\n</IMPORTANT>\n\n\nYou have access to the following functions:\n\n---- BEGIN FUNCTION #1: execute_bash ----\nDescription: Execute a bash command in the terminal.\n* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.\n* Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command=`ctrl+c` to interrupt the process.\n* Timeout: If a command execution result says "Command timed out. Sending SIGINT to the process", the assistant should retry running the command in the background.\n\nParameters:\n (1) command (string, required): The bash command to execute. Can be empty to view additional logs when previous exit code is `-1`. Can be `ctrl+c` to interrupt the currently running process.\n---- END FUNCTION #1 ----\n\n---- BEGIN FUNCTION #2: finish ----\nDescription: Finish the interaction when the task is complete OR if the assistant cannot proceed further with the task.\nNo parameters are required for this function.\n---- END FUNCTION #2 ----\n\n---- BEGIN FUNCTION #3: str_replace_editor ----\nDescription: Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* The `undo_edit` command will revert the last edit made to the file at `path`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`\n\nParameters:\n (1) command (string, required): The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.\nAllowed values: [`view`, `create`, `str_replace`, `insert`, `undo_edit`]\n (2) path (string, required): Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.\n (3) file_text (string, optional): Required parameter of `create` command, with the content of the file to be created.\n (4) old_str (string, optional): Required parameter of `str_replace` command containing the string in `path` to replace.\n (5) new_str (string, optional): Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.\n (6) insert_line (integer, optional): Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.\n (7) view_range (array, optional): Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.\n---- END FUNCTION #3 ----\n\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<function=example_function_name>\n<parameter=example_parameter_1>value_1</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format, start with <function= and end with </function>\n- Required parameters MUST be specified\n- Only call one function at a time\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after.\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>\n',
'text': 'You are a helpful assistant that can interact with a computer to solve tasks.\n<IMPORTANT>\n* If user provides a path, you should NOT assume it\'s relative to the current working directory. Instead, you should explore the file system to find the file before working on it.\n</IMPORTANT>\n\n\nYou have access to the following functions:\n\n---- BEGIN FUNCTION #1: execute_bash ----\nDescription: Execute a bash command in the terminal.\n* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.\n* Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command=`ctrl+c` to interrupt the process.\n* Timeout: If a command execution result says "Command timed out. Sending SIGINT to the process", the assistant should retry running the command in the background.\n\nParameters:\n (1) command (string, required): The bash command to execute. Can be empty to view additional logs when previous exit code is `-1`. Can be `ctrl+c` to interrupt the currently running process.\n---- END FUNCTION #1 ----\n\n---- BEGIN FUNCTION #2: finish ----\nDescription: Finish the interaction when the task is complete OR if the assistant cannot proceed further with the task.\nNo parameters are required for this function.\n---- END FUNCTION #2 ----\n\n---- BEGIN FUNCTION #3: str_replace_editor ----\nDescription: Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* The `undo_edit` command will revert the last edit made to the file at `path`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`\n\nParameters:\n (1) command (string, required): The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.\nAllowed values: [`view`, `create`, `str_replace`, `insert`, `undo_edit`]\n (2) path (string, required): Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.\n (3) file_text (string, optional): Required parameter of `create` command, with the content of the file to be created.\n (4) old_str (string, optional): Required parameter of `str_replace` command containing the string in `path` to replace.\n (5) new_str (string, optional): Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.\n (6) insert_line (integer, optional): Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.\n (7) view_range (array, optional): Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.\n---- END FUNCTION #3 ----\n\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<function=example_function_name>\n<parameter=example_parameter_1>value_1</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format, start with <function= and end with </function>\n- Required parameters MUST be specified\n- Only call one function at a time\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after.\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n',
'cache_control': {'type': 'ephemeral'},
}
],