feat: Add microagents UI to conversation context menu (#8984)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
Xingyao Wang 2025-06-11 11:12:27 -04:00 committed by GitHub
parent f27b02411b
commit 3f50eb0079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 734 additions and 47 deletions

View File

@ -11,6 +11,7 @@ import {
GetTrajectoryResponse,
GitChangeDiff,
GitChange,
GetMicroagentsResponse,
GetMicroagentPromptResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
@ -395,6 +396,21 @@ class OpenHands {
return data;
}
/**
* Get the available microagents associated with a conversation
* @param conversationId The ID of the conversation
* @returns The available microagents associated with the conversation
*/
static async getMicroagents(
conversationId: string,
): Promise<GetMicroagentsResponse> {
const url = `${this.getConversationUrl(conversationId)}/microagents`;
const { data } = await openHands.get<GetMicroagentsResponse>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
static async getMicroagentPrompt(
conversationId: string,
eventId: number,

View File

@ -103,6 +103,22 @@ export interface GitChangeDiff {
original: string;
}
export interface InputMetadata {
name: string;
description: string;
}
export interface Microagent {
name: string;
type: "repo" | "knowledge";
content: string;
triggers: string[];
}
export interface GetMicroagentsResponse {
microagents: Microagent[];
}
export interface GetMicroagentPromptResponse {
status: string;
prompt: string;

View File

@ -1,7 +1,9 @@
import { useTranslation } from "react-i18next";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { cn } from "#/utils/utils";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
import { I18nKey } from "#/i18n/declaration";
interface ConversationCardContextMenuProps {
onClose: () => void;
@ -9,6 +11,7 @@ interface ConversationCardContextMenuProps {
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onShowMicroagents?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
position?: "top" | "bottom";
}
@ -19,9 +22,11 @@ export function ConversationCardContextMenu({
onEdit,
onDisplayCost,
onShowAgentTools,
onShowMicroagents,
onDownloadViaVSCode,
position = "bottom",
}: ConversationCardContextMenuProps) {
const { t } = useTranslation();
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
return (
@ -68,6 +73,14 @@ export function ConversationCardContextMenu({
Show Agent Tools & Metadata
</ContextMenuListItem>
)}
{onShowMicroagents && (
<ContextMenuListItem
testId="show-microagents-button"
onClick={onShowMicroagents}
>
{t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
</ContextMenuListItem>
)}
</ContextMenu>
);
}

View File

@ -11,6 +11,7 @@ import {
import { EllipsisButton } from "./ellipsis-button";
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
import { SystemMessageModal } from "./system-message-modal";
import { MicroagentsModal } from "./microagents-modal";
import { cn } from "#/utils/utils";
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
@ -59,6 +60,8 @@ export function ConversationCard({
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
const [microagentsModalVisible, setMicroagentsModalVisible] =
React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const systemMessage = parsedEvents.find(isSystemMessage);
@ -142,6 +145,13 @@ export function ConversationCard({
setSystemModalVisible(true);
};
const handleShowMicroagents = (
event: React.MouseEvent<HTMLButtonElement>,
) => {
event.stopPropagation();
setMicroagentsModalVisible(true);
};
React.useEffect(() => {
if (titleMode === "edit") {
inputRef.current?.focus();
@ -225,6 +235,11 @@ export function ConversationCard({
? handleShowAgentTools
: undefined
}
onShowMicroagents={
showOptions && conversationId
? handleShowMicroagents
: undefined
}
position={variant === "compact" ? "top" : "bottom"}
/>
)}
@ -367,6 +382,13 @@ export function ConversationCard({
onClose={() => setSystemModalVisible(false)}
systemMessage={systemMessage ? systemMessage.args : null}
/>
{microagentsModalVisible && (
<MicroagentsModal
onClose={() => setMicroagentsModalVisible(false)}
conversationId={conversationId}
/>
)}
</>
);
}

View File

@ -0,0 +1,142 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, ChevronRight } from "lucide-react";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { I18nKey } from "#/i18n/declaration";
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
interface MicroagentsModalProps {
onClose: () => void;
conversationId: string | undefined;
}
export function MicroagentsModal({
onClose,
conversationId,
}: MicroagentsModalProps) {
const { t } = useTranslation();
const [expandedAgents, setExpandedAgents] = useState<Record<string, boolean>>(
{},
);
const {
data: microagents,
isLoading,
isError,
} = useConversationMicroagents({
conversationId,
enabled: true,
});
const toggleAgent = (agentName: string) => {
setExpandedAgents((prev) => ({
...prev,
[agentName]: !prev[agentName],
}));
};
return (
<ModalBackdrop onClose={onClose}>
<ModalBody
width="medium"
className="max-h-[80vh] flex flex-col items-start"
testID="microagents-modal"
>
<div className="flex flex-col gap-6 w-full">
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
</div>
<div className="w-full h-[60vh] overflow-auto rounded-md">
{isLoading && (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
</div>
)}
{!isLoading &&
(isError || !microagents || microagents.length === 0) && (
<div className="flex items-center justify-center h-full p-4">
<p className="text-gray-400">
{isError
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
</p>
</div>
)}
{!isLoading && microagents && microagents.length > 0 && (
<div className="p-2 space-y-3">
{microagents.map((agent) => {
const isExpanded = expandedAgents[agent.name] || false;
return (
<div key={agent.name} className="rounded-md overflow-hidden">
<button
type="button"
onClick={() => toggleAgent(agent.name)}
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<h3 className="font-bold text-gray-100">
{agent.name}
</h3>
</div>
<div className="flex items-center">
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
{agent.type === "repo" ? "Repository" : "Knowledge"}
</span>
<span className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</span>
</div>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
{agent.triggers && agent.triggers.length > 0 && (
<div className="mt-2 mb-3">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
</h4>
<div className="flex flex-wrap gap-1">
{agent.triggers.map((trigger) => (
<span
key={trigger}
className="px-2 py-1 text-xs rounded-full bg-blue-900"
>
{trigger}
</span>
))}
</div>
</div>
)}
<div className="mt-2">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
</h4>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
{agent.content ||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
</pre>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
</ModalBody>
</ModalBackdrop>
);
}

View File

@ -0,0 +1,25 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
interface UseConversationMicroagentsOptions {
conversationId: string | undefined;
enabled?: boolean;
}
export const useConversationMicroagents = ({
conversationId,
enabled = true,
}: UseConversationMicroagentsOptions) =>
useQuery({
queryKey: ["conversation", conversationId, "microagents"],
queryFn: async () => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
const data = await OpenHands.getMicroagents(conversationId);
return data.microagents;
},
enabled: !!conversationId && enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@ -1,15 +1,5 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
MICROAGENT$NO_REPOSITORY_FOUND = "MICROAGENT$NO_REPOSITORY_FOUND",
MICROAGENT$ADD_TO_MICROAGENT = "MICROAGENT$ADD_TO_MICROAGENT",
MICROAGENT$WHAT_TO_ADD = "MICROAGENT$WHAT_TO_ADD",
MICROAGENT$WHERE_TO_PUT = "MICROAGENT$WHERE_TO_PUT",
MICROAGENT$ADD_TRIGGER = "MICROAGENT$ADD_TRIGGER",
MICROAGENT$WAIT_FOR_RUNTIME = "MICROAGENT$WAIT_FOR_RUNTIME",
MICROAGENT$ADDING_CONTEXT = "MICROAGENT$ADDING_CONTEXT",
MICROAGENT$VIEW_CONVERSATION = "MICROAGENT$VIEW_CONVERSATION",
MICROAGENT$SUCCESS_PR_READY = "MICROAGENT$SUCCESS_PR_READY",
STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME",
STATUS$WEBSOCKET_CLOSED = "STATUS$WEBSOCKET_CLOSED",
HOME$LAUNCH_FROM_SCRATCH = "HOME$LAUNCH_FROM_SCRATCH",
HOME$READ_THIS = "HOME$READ_THIS",
@ -376,6 +366,7 @@ export enum I18nKey {
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
SIDEBAR$CONVERSATIONS = "SIDEBAR$CONVERSATIONS",
STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME",
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
STATUS$STARTING_CONTAINER = "STATUS$STARTING_CONTAINER",
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
@ -554,6 +545,16 @@ export enum I18nKey {
TOS$CONTINUE = "TOS$CONTINUE",
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
TIPS$CUSTOMIZE_MICROAGENT = "TIPS$CUSTOMIZE_MICROAGENT",
CONVERSATION$SHOW_MICROAGENTS = "CONVERSATION$SHOW_MICROAGENTS",
CONVERSATION$NO_MICROAGENTS = "CONVERSATION$NO_MICROAGENTS",
CONVERSATION$FAILED_TO_FETCH_MICROAGENTS = "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS",
MICROAGENTS_MODAL$TITLE = "MICROAGENTS_MODAL$TITLE",
MICROAGENTS_MODAL$TRIGGERS = "MICROAGENTS_MODAL$TRIGGERS",
MICROAGENTS_MODAL$INPUTS = "MICROAGENTS_MODAL$INPUTS",
MICROAGENTS_MODAL$TOOLS = "MICROAGENTS_MODAL$TOOLS",
MICROAGENTS_MODAL$CONTENT = "MICROAGENTS_MODAL$CONTENT",
MICROAGENTS_MODAL$NO_CONTENT = "MICROAGENTS_MODAL$NO_CONTENT",
MICROAGENTS_MODAL$FETCH_ERROR = "MICROAGENTS_MODAL$FETCH_ERROR",
TIPS$SETUP_SCRIPT = "TIPS$SETUP_SCRIPT",
TIPS$VSCODE_INSTANCE = "TIPS$VSCODE_INSTANCE",
TIPS$SAVE_WORK = "TIPS$SAVE_WORK",

View File

@ -8704,20 +8704,180 @@
"tr": "Hizmet Şartlarını kabul ederken hata oluştu"
},
"TIPS$CUSTOMIZE_MICROAGENT": {
"en": "You can customize OpenHands for your repo using a microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
"ja": "マイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
"zh-CN": "您可以使用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述包括如何运行代码放入.openhands/microagents/repo.md。",
"zh-TW": "您可以使用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述包括如何運行代碼放入.openhands/microagents/repo.md。",
"ko-KR": "마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
"tr": "Bir mikro ajan kullanarak deponuz için OpenHands'i özelleştirebilirsiniz. OpenHands'ten kodun nasıl çalıştırılacağı da dahil olmak üzere deponun açıklamasını .openhands/microagents/repo.md dosyasına koymasını isteyin.",
"de": "Sie können OpenHands für Ihr Repository mit einem Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.",
"uk": "Ви можете налаштувати OpenHands для вашого репозиторію за допомогою мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інструкції з запуску коду, у файлі .openhands/microagents/repo.md."
"en": "You can customize OpenHands for your repo using an available microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",
"ja": "利用可能なマイクロエージェントを使用して、リポジトリ用にOpenHandsをカスタマイズできます。OpenHandsに、コードの実行方法を含むリポジトリの説明を.openhands/microagents/repo.mdに入れるよう依頼してください。",
"zh-CN": "您可以使用可用微代理为您的仓库自定义OpenHands。请OpenHands将仓库的描述包括如何运行代码放入.openhands/microagents/repo.md。",
"zh-TW": "您可以使用可用微代理為您的倉庫自定義OpenHands。請OpenHands將倉庫的描述包括如何運行代碼放入.openhands/microagents/repo.md。",
"ko-KR": "사용 가능한 마이크로에이전트를 사용하여 저장소에 맞게 OpenHands를 사용자 정의할 수 있습니다. OpenHands에게 코드 실행 방법을 포함한 저장소 설명을 .openhands/microagents/repo.md에 넣도록 요청하세요.",
"no": "Du kan tilpasse OpenHands for ditt repo ved å bruke en tilgjengelig mikroagent. Be OpenHands om å legge en beskrivelse av repoet, inkludert hvordan du kjører koden, i .openhands/microagents/repo.md.",
"it": "Puoi personalizzare OpenHands per il tuo repository utilizzando un microagente disponibile. Chiedi a OpenHands di inserire una descrizione del repository, incluso come eseguire il codice, in .openhands/microagents/repo.md.",
"pt": "Você pode personalizar o OpenHands para seu repositório usando um microagente disponível. Peça ao OpenHands para colocar uma descrição do repositório, incluindo como executar o código, em .openhands/microagents/repo.md.",
"es": "Puede personalizar OpenHands para su repositorio utilizando un microagente disponible. Pídale a OpenHands que ponga una descripción del repositorio, incluido cómo ejecutar el código, en .openhands/microagents/repo.md.",
"ar": "يمكنك تخصيص OpenHands لمستودعك باستخدام وكيل مصغر متاح. اطلب من OpenHands وضع وصف للمستودع، بما في ذلك كيفية تشغيل الكود، في .openhands/microagents/repo.md.",
"de": "Sie können OpenHands für Ihr Repository mit einem verfügbaren Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.",
"fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent disponible. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.",
"tr": "Kullanılabilir bir mikro ajan kullanarak OpenHands'i deponuz için özelleştirebilirsiniz. OpenHands'ten deponun açıklamasını, kodun nasıl çalıştırılacağı dahil, .openhands/microagents/repo.md dosyasına koymasını isteyin.",
"uk": "Ви можете налаштувати OpenHands для свого репозиторію за допомогою доступного мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інформацію про те, як запустити код, у файлі .openhands/microagents/repo.md."
},
"CONVERSATION$SHOW_MICROAGENTS": {
"en": "Show Available Microagents",
"ja": "利用可能なマイクロエージェントを表示",
"zh-CN": "显示可用微代理",
"zh-TW": "顯示可用微代理",
"ko-KR": "사용 가능한 마이크로에이전트 표시",
"no": "Vis tilgjengelige mikroagenter",
"ar": "عرض الوكلاء المصغرين المتاحة",
"de": "Verfügbare Mikroagenten anzeigen",
"fr": "Afficher les micro-agents disponibles",
"it": "Mostra microagenti disponibili",
"pt": "Mostrar microagentes disponíveis",
"es": "Mostrar microagentes disponibles",
"tr": "Kullanılabilir mikro ajanları göster",
"uk": "Показати доступних мікроагентів"
},
"CONVERSATION$NO_MICROAGENTS": {
"en": "No available microagents found for this conversation.",
"ja": "この会話用の利用可能なマイクロエージェントが見つかりませんでした。",
"zh-CN": "未找到此对话的可用微代理。",
"zh-TW": "未找到此對話的可用微代理。",
"ko-KR": "이 대화에 대한 사용 가능한 마이크로에이전트를 찾을 수 없습니다.",
"no": "Ingen tilgjengelige mikroagenter funnet for denne samtalen.",
"ar": "لم يتم العثور على وكلاء مصغرين متاحة لهذه المحادثة.",
"de": "Keine verfügbaren Mikroagenten für dieses Gespräch gefunden.",
"fr": "Aucun micro-agent disponible trouvé pour cette conversation.",
"it": "Nessun microagente disponibile trovato per questa conversazione.",
"pt": "Nenhum microagente disponível encontrado para esta conversa.",
"es": "No se encontraron microagentes disponibles para esta conversación.",
"tr": "Bu konuşma için kullanılabilir mikro ajan bulunamadı.",
"uk": "Для цієї розмови не знайдено доступних мікроагентів."
},
"CONVERSATION$FAILED_TO_FETCH_MICROAGENTS": {
"en": "Failed to fetch available microagents",
"ja": "利用可能なマイクロエージェントの取得に失敗しました",
"zh-CN": "获取可用微代理失败",
"zh-TW": "獲取可用微代理失敗",
"ko-KR": "사용 가능한 마이크로에이전트를 가져오지 못했습니다",
"no": "Kunne ikke hente tilgjengelige mikroagenter",
"ar": "فشل في جلب الوكلاء المصغرين المتاحة",
"de": "Fehler beim Abrufen von verfügbaren Mikroagenten",
"fr": "Échec de la récupération des micro-agents disponibles",
"it": "Impossibile recuperare i microagenti disponibili",
"pt": "Falha ao buscar microagentes disponíveis",
"es": "Error al obtener microagentes disponibles",
"tr": "Kullanılabilir mikro ajanlar getirilemedi",
"uk": "Не вдалося отримати доступних мікроагентів"
},
"MICROAGENTS_MODAL$TITLE": {
"en": "Available Microagents",
"ja": "利用可能なマイクロエージェント",
"zh-CN": "可用微代理",
"zh-TW": "可用微代理",
"ko-KR": "사용 가능한 마이크로에이전트",
"no": "Tilgjengelige mikroagenter",
"ar": "الوكلاء المصغرين المتاحة",
"de": "Verfügbare Mikroagenten",
"fr": "Micro-agents disponibles",
"it": "Microagenti disponibili",
"pt": "Microagentes disponíveis",
"es": "Microagentes disponibles",
"tr": "Kullanılabilir mikro ajanlar",
"uk": "Доступні мікроагенти"
},
"MICROAGENTS_MODAL$TRIGGERS": {
"en": "Triggers",
"ja": "トリガー",
"zh-CN": "触发器",
"zh-TW": "觸發器",
"ko-KR": "트리거",
"no": "Utløsere",
"ar": "المحفزات",
"de": "Auslöser",
"fr": "Déclencheurs",
"it": "Trigger",
"pt": "Gatilhos",
"es": "Disparadores",
"tr": "Tetikleyiciler",
"uk": "Тригери"
},
"MICROAGENTS_MODAL$INPUTS": {
"en": "Inputs",
"ja": "入力",
"zh-CN": "输入",
"zh-TW": "輸入",
"ko-KR": "입력",
"no": "Inndata",
"ar": "المدخلات",
"de": "Eingaben",
"fr": "Entrées",
"it": "Input",
"pt": "Entradas",
"es": "Entradas",
"tr": "Girdiler",
"uk": "Вхідні дані"
},
"MICROAGENTS_MODAL$TOOLS": {
"en": "Tools",
"ja": "ツール",
"zh-CN": "工具",
"zh-TW": "工具",
"ko-KR": "도구",
"no": "Verktøy",
"ar": "الأدوات",
"de": "Werkzeuge",
"fr": "Outils",
"it": "Strumenti",
"pt": "Ferramentas",
"es": "Herramientas",
"tr": "Araçlar",
"uk": "Інструменти"
},
"MICROAGENTS_MODAL$CONTENT": {
"en": "Content",
"ja": "コンテンツ",
"zh-CN": "内容",
"zh-TW": "內容",
"ko-KR": "콘텐츠",
"no": "Innhold",
"ar": "المحتوى",
"de": "Inhalt",
"fr": "Contenu",
"it": "Contenuto",
"pt": "Conteúdo",
"es": "Contenido",
"tr": "İçerik",
"uk": "Вміст"
},
"MICROAGENTS_MODAL$NO_CONTENT": {
"en": "Microagent has no content",
"ja": "マイクロエージェントにコンテンツがありません",
"zh-CN": "微代理没有内容",
"zh-TW": "微代理沒有內容",
"ko-KR": "마이크로에이전트에 콘텐츠가 없습니다",
"no": "Mikroagenten har ikke innhold",
"ar": "الوكيل المصغر ليس لديه محتوى",
"de": "Mikroagent hat keinen Inhalt",
"fr": "Le micro-agent n'a pas de contenu",
"it": "Il microagente non ha contenuto",
"pt": "Microagente não tem conteúdo",
"es": "El microagente no tiene contenido",
"tr": "Mikroajanın içeriği yok",
"uk": "Мікроагент не має вмісту"
},
"MICROAGENTS_MODAL$FETCH_ERROR": {
"en": "Failed to fetch microagents. Please try again later.",
"ja": "マイクロエージェントの取得に失敗しました。後でもう一度お試しください。",
"zh-CN": "获取微代理失败。请稍后再试。",
"zh-TW": "獲取微代理失敗。請稍後再試。",
"ko-KR": "마이크로에이전트를 가져오지 못했습니다. 나중에 다시 시도해 주세요.",
"no": "Kunne ikke hente mikroagenter. Prøv igjen senere.",
"ar": "فشل في جلب الوكلاء المصغرين. يرجى المحاولة مرة أخرى لاحقًا.",
"de": "Mikroagenten konnten nicht abgerufen werden. Bitte versuchen Sie es später erneut.",
"fr": "Échec de la récupération des micro-agents. Veuillez réessayer plus tard.",
"it": "Impossibile recuperare i microagenti. Riprova più tardi.",
"pt": "Falha ao buscar microagentes. Por favor, tente novamente mais tarde.",
"es": "Error al obtener microagentes. Por favor, inténtelo de nuevo más tarde.",
"tr": "Mikroajanlar getirilemedi. Lütfen daha sonra tekrar deneyin.",
"uk": "Не вдалося отримати мікроагентів. Будь ласка, спробуйте пізніше."
},
"TIPS$SETUP_SCRIPT": {
"en": "You can add .openhands/setup.sh to your repository to automatically run a setup script every time you start an OpenHands conversation.",

View File

@ -9,6 +9,7 @@ from openhands.events.action import MessageAction
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.agent_session import AgentSession
from openhands.server.session.conversation import ServerConversation
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.settings import Settings
@ -114,6 +115,17 @@ class ConversationManager(ABC):
async def close_session(self, sid: str):
"""Close a session."""
@abstractmethod
def get_agent_session(self, sid: str) -> AgentSession | None:
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
@abstractmethod
async def get_agent_loop_info(
self, user_id: str | None = None, filter_to_sids: set[str] | None = None

View File

@ -307,7 +307,9 @@ class DockerNestedConversationManager(ConversationManager):
await asyncio.sleep(1)
except Exception as e:
logger.warning('error_stopping_container', extra={"sid": sid, "error": str(e)})
logger.warning(
'error_stopping_container', extra={'sid': sid, 'error': str(e)}
)
container.stop()
async def get_agent_loop_info(
@ -364,6 +366,15 @@ class DockerNestedConversationManager(ConversationManager):
file_store=file_store,
)
def get_agent_session(self, sid: str):
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
raise ValueError('unsupported_operation')
async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
conversation_store_class = self._conversation_store_class
if not conversation_store_class:

View File

@ -15,7 +15,7 @@ from openhands.events.stream import EventStreamSubscriber, session_exists
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE
from openhands.server.session.agent_session import AgentSession, WAIT_TIME_BEFORE_CLOSE
from openhands.server.session.conversation import ServerConversation
from openhands.server.session.session import ROOM_KEY, Session
from openhands.storage.conversation.conversation_store import ConversationStore
@ -112,7 +112,7 @@ class StandaloneConversationManager(ConversationManager):
end_time = time.time()
logger.info(
f'ServerConversation {c.sid} connected in {end_time - start_time} seconds',
extra={'session_id': sid}
extra={'session_id': sid},
)
self._active_conversations[sid] = (c, 1)
return c
@ -356,6 +356,20 @@ class StandaloneConversationManager(ConversationManager):
if session:
await self._close_session(sid)
def get_agent_session(self, sid: str) -> AgentSession | None:
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
session = self._local_agent_loops_by_sid.get(sid)
if session:
return session.agent_session
return None
async def _close_session(self, sid: str):
logger.info(f'_close_session:{sid}', extra={'session_id': sid})

View File

@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events.event_filter import EventFilter
@ -9,12 +10,18 @@ from openhands.server.dependencies import get_dependencies
from openhands.server.session.conversation import ServerConversation
from openhands.server.shared import conversation_manager
from openhands.server.utils import get_conversation
from openhands.microagent.types import InputMetadata
from openhands.memory.memory import Memory
app = APIRouter(prefix='/api/conversations/{conversation_id}', dependencies=get_dependencies())
app = APIRouter(
prefix='/api/conversations/{conversation_id}', dependencies=get_dependencies()
)
@app.get('/config')
async def get_remote_runtime_config(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
async def get_remote_runtime_config(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Retrieve the runtime configuration.
Currently, this is the session ID and runtime ID (if available).
@ -31,7 +38,9 @@ async def get_remote_runtime_config(conversation: ServerConversation = Depends(g
@app.get('/vscode-url')
async def get_vscode_url(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
async def get_vscode_url(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Get the VSCode URL.
This endpoint allows getting the VSCode URL.
@ -61,7 +70,9 @@ async def get_vscode_url(conversation: ServerConversation = Depends(get_conversa
@app.get('/web-hosts')
async def get_hosts(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
async def get_hosts(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Get the hosts used by the runtime.
This endpoint allows getting the hosts used by the runtime.
@ -143,7 +154,92 @@ async def search_events(
@app.post('/events')
async def add_event(request: Request, conversation: ServerConversation = Depends(get_conversation)):
async def add_event(
request: Request, conversation: ServerConversation = Depends(get_conversation)
):
data = request.json()
await conversation_manager.send_to_event_stream(conversation.sid, data)
return JSONResponse({'success': True})
class MicroagentResponse(BaseModel):
"""Response model for microagents endpoint."""
name: str
type: str
content: str
triggers: list[str] = []
inputs: list[InputMetadata] = []
tools: list[str] = []
@app.get('/microagents')
async def get_microagents(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Get all microagents associated with the conversation.
This endpoint returns all repository and knowledge microagents that are loaded for the conversation.
Returns:
JSONResponse: A JSON response containing the list of microagents.
"""
try:
# Get the agent session for this conversation
agent_session = conversation_manager.get_agent_session(conversation.sid)
if not agent_session:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={'error': 'Agent session not found for this conversation'},
)
# Access the memory to get the microagents
memory: Memory | None = agent_session.memory
if memory is None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
'error': 'Memory is not yet initialized for this conversation'
},
)
# Prepare the response
microagents = []
# Add repo microagents
for name, agent in memory.repo_microagents.items():
microagents.append(
MicroagentResponse(
name=name,
type='repo',
content=agent.content,
triggers=[],
inputs=agent.metadata.inputs,
tools=[server.name for server in agent.metadata.mcp_tools.stdio_servers] if agent.metadata.mcp_tools else [],
)
)
# Add knowledge microagents
for name, agent in memory.knowledge_microagents.items():
microagents.append(
MicroagentResponse(
name=name,
type='knowledge',
content=agent.content,
triggers=agent.triggers,
inputs=agent.metadata.inputs,
tools=[server.name for server in agent.metadata.mcp_tools.stdio_servers] if agent.metadata.mcp_tools else [],
)
)
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'microagents': [m.dict() for m in microagents]},
)
except Exception as e:
logger.error(f'Error getting microagents: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': f'Error getting microagents: {e}'},
)

View File

@ -51,6 +51,7 @@ class AgentSession:
controller: AgentController | None = None
runtime: Runtime | None = None
security_analyzer: SecurityAnalyzer | None = None
memory: Memory | None = None
_starting: bool = False
_started_at: float = 0
_closed: bool = False

30
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiofiles"
@ -400,7 +400,7 @@ description = "LTS Port of Python audioop"
optional = false
python-versions = ">=3.13"
groups = ["main"]
markers = "python_version >= \"3.13\""
markers = "python_version == \"3.13\""
files = [
{file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"},
{file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"},
@ -1580,7 +1580,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "comm"
@ -2974,8 +2974,8 @@ files = [
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
proto-plus = [
{version = ">=1.22.3,<2.0.0dev"},
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
@ -2997,8 +2997,8 @@ googleapis-common-protos = ">=1.56.2,<2.0.0"
grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
requests = ">=2.18.0,<3.0.0"
@ -3017,6 +3017,7 @@ optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_python_client-2.171.0-py3-none-any.whl", hash = "sha256:c9c9b76f561e9d9ac14e54a9e2c0842876201d5b96e69e48f967373f0784cbe9"},
{file = "google_api_python_client-2.171.0.tar.gz", hash = "sha256:057a5c08d28463c6b9eb89746355de5f14b7ed27a65c11fdbf1d06c66bb66b23"},
]
@ -3215,8 +3216,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpc-google-iam-v1 = ">=0.14.0,<1.0.0"
proto-plus = [
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
@ -3896,7 +3897,7 @@ version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
groups = ["evaluation", "test"]
groups = ["dev", "evaluation", "test"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
@ -6481,8 +6482,8 @@ files = [
[package.dependencies]
googleapis-common-protos = ">=1.52,<2.0"
grpcio = [
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
{version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
]
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.34.0"
@ -6902,7 +6903,7 @@ version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["evaluation", "test"]
groups = ["dev", "evaluation", "test"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
@ -7667,7 +7668,7 @@ version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["main", "evaluation", "runtime", "test"]
groups = ["main", "dev", "evaluation", "runtime", "test"]
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
@ -7820,7 +7821,7 @@ version = "8.4.0"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.9"
groups = ["evaluation", "test"]
groups = ["dev", "evaluation", "test"]
files = [
{file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"},
{file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"},
@ -9244,7 +9245,6 @@ files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
]
markers = {evaluation = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
@ -9487,7 +9487,7 @@ description = "Standard library aifc redistribution. \"dead battery\"."
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version >= \"3.13\""
markers = "python_version == \"3.13\""
files = [
{file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"},
{file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"},
@ -9504,7 +9504,7 @@ description = "Standard library chunk redistribution. \"dead battery\"."
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version >= \"3.13\""
markers = "python_version == \"3.13\""
files = [
{file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"},
{file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"},
@ -11666,4 +11666,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "c9a787a0d9eebd64f46961ad726ac0a75a7420f0fcb22f87f2bf689d1ccb389c"
content-hash = "beb348a8a07835531e5dba2c8807fdf68669fe239c630d0420dc204b3b2648ed"

View File

@ -95,6 +95,7 @@ mypy = "1.16.0"
pre-commit = "4.2.0"
build = "*"
types-setuptools = "*"
pytest = "^8.4.0"
[tool.poetry.group.test]
optional = true

View File

@ -0,0 +1,157 @@
import json
from unittest.mock import MagicMock, patch
import pytest
from fastapi.responses import JSONResponse
from openhands.microagent.microagent import KnowledgeMicroagent, RepoMicroagent
from openhands.microagent.types import MicroagentMetadata, MicroagentType
from openhands.server.routes.conversation import get_microagents
from openhands.server.session.conversation import ServerConversation
@pytest.mark.asyncio
async def test_get_microagents():
"""Test the get_microagents function directly."""
# Create mock microagents
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
repo_microagent = RepoMicroagent(
name='test_repo',
content='This is a test repo microagent',
metadata=MicroagentMetadata(
name='test_repo',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[], # Empty inputs to match the expected behavior
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='git', command='git'),
MCPStdioServerConfig(name='file_editor', command='editor'),
]
),
),
source='test_source',
type=MicroagentType.REPO_KNOWLEDGE,
)
knowledge_microagent = KnowledgeMicroagent(
name='test_knowledge',
content='This is a test knowledge microagent',
metadata=MicroagentMetadata(
name='test_knowledge',
type=MicroagentType.KNOWLEDGE,
triggers=['test', 'knowledge'],
inputs=[], # Empty inputs to match the expected behavior
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='search', command='search'),
MCPStdioServerConfig(name='fetch', command='fetch'),
]
),
),
source='test_source',
type=MicroagentType.KNOWLEDGE,
)
# Mock the agent session and memory
mock_memory = MagicMock()
mock_memory.repo_microagents = {'test_repo': repo_microagent}
mock_memory.knowledge_microagents = {'test_knowledge': knowledge_microagent}
mock_agent_session = MagicMock()
mock_agent_session.memory = mock_memory
# Create a mock ServerConversation
mock_conversation = MagicMock(spec=ServerConversation)
mock_conversation.sid = 'test_sid'
# Mock the conversation manager
with patch(
'openhands.server.routes.conversation.conversation_manager'
) as mock_manager:
# Set up the mocks
mock_manager.get_agent_session.return_value = mock_agent_session
# Call the function directly
response = await get_microagents(conversation=mock_conversation)
# Verify the response
assert isinstance(response, JSONResponse)
assert response.status_code == 200
# Parse the JSON content
content = json.loads(response.body)
assert 'microagents' in content
assert len(content['microagents']) == 2
# Check repo microagent
repo_agent = next(m for m in content['microagents'] if m['name'] == 'test_repo')
assert repo_agent['type'] == 'repo'
assert repo_agent['content'] == 'This is a test repo microagent'
assert repo_agent['triggers'] == []
assert repo_agent['inputs'] == [] # Expect empty inputs
assert repo_agent['tools'] == ['git', 'file_editor']
# Check knowledge microagent
knowledge_agent = next(
m for m in content['microagents'] if m['name'] == 'test_knowledge'
)
assert knowledge_agent['type'] == 'knowledge'
assert knowledge_agent['content'] == 'This is a test knowledge microagent'
assert knowledge_agent['triggers'] == ['test', 'knowledge']
assert knowledge_agent['inputs'] == [] # Expect empty inputs
assert knowledge_agent['tools'] == ['search', 'fetch']
@pytest.mark.asyncio
async def test_get_microagents_no_agent_session():
"""Test the get_microagents function when no agent session is found."""
# Create a mock ServerConversation
mock_conversation = MagicMock(spec=ServerConversation)
mock_conversation.sid = 'test_sid'
# Mock the conversation manager
with patch(
'openhands.server.routes.conversation.conversation_manager'
) as mock_manager:
# Set up the mocks
mock_manager.get_agent_session.return_value = None
# Call the function directly
response = await get_microagents(conversation=mock_conversation)
# Verify the response
assert isinstance(response, JSONResponse)
assert response.status_code == 404
# Parse the JSON content
content = json.loads(response.body)
assert 'error' in content
assert 'Agent session not found' in content['error']
@pytest.mark.asyncio
async def test_get_microagents_exception():
"""Test the get_microagents function when an exception occurs."""
# Create a mock ServerConversation
mock_conversation = MagicMock(spec=ServerConversation)
mock_conversation.sid = 'test_sid'
# Mock the conversation manager
with patch(
'openhands.server.routes.conversation.conversation_manager'
) as mock_manager:
# Set up the mocks to raise an exception
mock_manager.get_agent_session.side_effect = Exception('Test exception')
# Call the function directly
response = await get_microagents(conversation=mock_conversation)
# Verify the response
assert isinstance(response, JSONResponse)
assert response.status_code == 500
# Parse the JSON content
content = json.loads(response.body)
assert 'error' in content
assert 'Test exception' in content['error']