-
+
@@ -334,20 +458,22 @@ defineExpose({
-
-
-
-
- {{ t('phaseExecuting') }}
- {{ t('phaseEvaluating') }}
-
-
-
- {{ t('testRunning') }}
-
-
- {{ statusDescription }}
+ class="h-40 min-h-40 flex-shrink-0 border-t-1 border-auxiliar-gray p-4">
+
+
+
+
+
+ {{ t('phaseExecuting') }}
+ {{ t('phaseEvaluating') }}
+
+
+
+ {{ t('testRunning') }}
+
+
+ {{ statusDescription }}
+
@@ -356,7 +482,8 @@ defineExpose({
-
{
+
+{
"en": {
"newTestCaseTitle": "Create a new test case",
"editTestCaseTitle": "Editing: {testCaseName}",
@@ -368,14 +495,19 @@ defineExpose({
"successDescription": "The agent's response matched the expected output. No formatting or content deviations were detected.",
"failureDescription": "The agent's response did not match the expected output. Formatting or content deviations were detected.",
"errorDescription": "An error occurred while running the test case",
- "noRunResultsDescription": "Run the full suite to validate all defined test cases.",
- "noTestCaseResultsDescription": "Run the test to compare the agent's response with the expected output.",
- "obtainedResultLabel": "Obtained result",
"runningTestCase": "Running: {testCaseName}",
"testCaseResult": "Test case result: {testCaseName}",
"testRunning": "Test is running...",
"phaseExecuting": "Executing test case...",
- "phaseEvaluating": "Evaluating response..."
+ "phaseEvaluating": "Evaluating response...",
+ "compare": "Compare with specification",
+ "closeCompare": "Close compare",
+ "compareDisabledReasonTestModified": "Compare is disabled because the test case was modified after the test execution",
+ "compareDisabledReasonTestDeleted": "Compare is disabled because the test case was deleted",
+ "success": "Success",
+ "failure": "Failed",
+ "error": "Error running",
+ "skipped": "Skipped"
},
"es": {
"newTestCaseTitle": "Crea un nuevo test case",
@@ -388,13 +520,19 @@ defineExpose({
"successDescription": "La respuesta del agente coincidió con la salida esperada. No se detectaron desvíos de formato o contenido.",
"failureDescription": "La respuesta del agente no coincidió con la salida esperada. Se detectaron desvíos de formato o contenido.",
"errorDescription": "Ocurrió un error al ejecutar el test case",
- "noRunResultsDescription": "Corre la suite completa para validar todos los test cases definidos.",
- "noTestCaseResultsDescription": "Ejecuta el test para comparar la respuesta del agente con la esperada.",
- "obtainedResultLabel": "Resultado obtenido",
"runningTestCase": "Ejecutando: {testCaseName}",
"testCaseResult": "Resultado del test case: {testCaseName}",
"testRunning": "El test está ejecutándose...",
"phaseExecuting": "Ejecutando test case...",
- "phaseEvaluating": "Evaluando respuesta..."
+ "phaseEvaluating": "Evaluando respuesta...",
+ "compare": "Comparar con la especificación",
+ "closeCompare": "Cerrar comparación",
+ "compareDisabledReasonTestModified": "La comparación está deshabilitada porque el test case fue modificado después de la ejecución del test",
+ "compareDisabledReasonTestDeleted": "La comparación está deshabilitada porque el test case fue eliminado",
+ "success": "Pasó",
+ "failure": "Falló",
+ "error": "Error al ejecutar",
+ "skipped": "Omitido"
}
-}
+}
+
diff --git a/src/frontend/src/components/agent/AgentTestcaseResults.vue b/src/frontend/src/components/agent/AgentTestcaseResults.vue
new file mode 100644
index 0000000..26f46aa
--- /dev/null
+++ b/src/frontend/src/components/agent/AgentTestcaseResults.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('stopSuiteRun') }}
+
+
+
+
+
+
+
+
+
+
+
{{ result.testCaseName }}
+
+
+
+
+
+
+
+
+
+{
+ "en": {
+ "running": "Running...",
+ "runResultTitle": "Results from {'<'}b>{date}{'<'}/b>",
+ "stopSuiteRun": "Stop"
+ },
+ "es": {
+ "running": "Ejecutando...",
+ "runResultTitle": "Resultados de {'<'}b>{date}{'<'}/b>",
+ "stopSuiteRun": "Detener"
+ }
+}
+
diff --git a/src/frontend/src/components/agent/AgentTestcaseResultsSkeleton.vue b/src/frontend/src/components/agent/AgentTestcaseResultsSkeleton.vue
new file mode 100644
index 0000000..72c983e
--- /dev/null
+++ b/src/frontend/src/components/agent/AgentTestcaseResultsSkeleton.vue
@@ -0,0 +1,39 @@
+
+
+
diff --git a/src/frontend/src/components/agent/AgentTestcaseRunStatus.vue b/src/frontend/src/components/agent/AgentTestcaseRunStatus.vue
new file mode 100644
index 0000000..1e8768a
--- /dev/null
+++ b/src/frontend/src/components/agent/AgentTestcaseRunStatus.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ {{ passed }}
+
+
+
+ {{ failed }}
+
+
+
+ {{ error }}
+
+
+
+ {{ skipped }}
+
+
+
+
+
+{
+ "en": {
+ "passed": "Passed",
+ "failed": "Failed",
+ "error": "Error",
+ "skipped": "Skipped"
+ },
+ "es": {
+ "passed": "Pasó",
+ "failed": "Falló",
+ "error": "Error",
+ "skipped": "Omitido"
+ }
+}
+
diff --git a/src/frontend/src/components/agent/TestCaseStatus.vue b/src/frontend/src/components/agent/AgentTestcaseStatus.vue
similarity index 98%
rename from src/frontend/src/components/agent/TestCaseStatus.vue
rename to src/frontend/src/components/agent/AgentTestcaseStatus.vue
index a4ea1c1..5b2fe34 100644
--- a/src/frontend/src/components/agent/TestCaseStatus.vue
+++ b/src/frontend/src/components/agent/AgentTestcaseStatus.vue
@@ -67,7 +67,8 @@ const statusConfig = computed(() => {
-
{
+
+{
"en": {
"running": "Running",
"success": "Success",
@@ -84,5 +85,5 @@ const statusConfig = computed(() => {
"pending": "Pendiente",
"skipped": "Omitido"
}
-}
-
+}
+
diff --git a/src/frontend/src/components/agent/AgentTestcases.vue b/src/frontend/src/components/agent/AgentTestcases.vue
new file mode 100644
index 0000000..831ae44
--- /dev/null
+++ b/src/frontend/src/components/agent/AgentTestcases.vue
@@ -0,0 +1,238 @@
+
+
+
+
+ {{ t('noTestCasesTitle') }}
+ {{ t('noTestCasesDescription') }}
+
+
+
+
+
+
+
+ {{ t('runTestsButton') }}
+
+
+
+ {{ t('newTestCaseButton') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ testCase.thread.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{
+ "en": {
+ "newTestCaseButton": "Add",
+ "renameTestCaseTooltip": "Rename",
+ "cloneTestCaseTooltip": "Clone",
+ "deleteTestCaseTooltip": "Delete",
+ "deleteTestCaseConfirmation": "Delete {testCaseName}?",
+ "runTestCaseMenuItem": "Run",
+ "runTestsButton": "Run all",
+ "pastExecutions": "Past Executions",
+ "noTestCasesTitle": "You don't have test cases for this agent yet",
+ "noTestCasesDescription": "Create your first test case to validate that the agent meets the expected requirements.",
+ "configureEvaluator": "Configure agent evaluator",
+ "configureTestEvaluator": "Configure test evaluator"
+ },
+ "es": {
+ "newTestCaseButton": "Agregar",
+ "renameTestCaseTooltip": "Renombrar",
+ "cloneTestCaseTooltip": "Clonar",
+ "deleteTestCaseTooltip": "Eliminar",
+ "deleteTestCaseConfirmation": "¿Eliminar {testCaseName}?",
+ "runTestCaseMenuItem": "Ejecutar",
+ "runTestsButton": "Ejecutar todos",
+ "pastExecutions": "Ejecuciones pasadas",
+ "noTestCasesTitle": "Aún no tienes test cases para este agente",
+ "noTestCasesDescription": "Crea tu primer test case para validar que el agente cumple los requisitos esperados.",
+ "configureEvaluator": "Configurar evaluador del agente",
+ "configureTestEvaluator": "Configurar evaluador del test"
+ }
+}
+
diff --git a/src/frontend/src/components/agent/AgentToolConfigEditor.vue b/src/frontend/src/components/agent/AgentToolConfigEditor.vue
index d70649d..9ef1f25 100644
--- a/src/frontend/src/components/agent/AgentToolConfigEditor.vue
+++ b/src/frontend/src/components/agent/AgentToolConfigEditor.vue
@@ -5,7 +5,7 @@ import type { JSONSchema7, JSONSchema7Definition } from 'json-schema'
import { useErrorHandler } from '@/composables/useErrorHandler'
import Ajv, { type ErrorObject } from 'ajv'
import addFormats from 'ajv-formats'
-import { AuthenticationWindowCloseError, AuthenticationCancelError, handleOAuthRequestsIn } from '@/services/toolOAuth';
+import { AuthenticationError, handleOAuthRequestsIn } from '@/services/toolOAuth';
import { AgentToolConfig, AgentTool } from '@/services/api'
export class EditingToolConfig {
@@ -116,11 +116,15 @@ const toolMessage = computed(() => {
return ret != toolMessageKey ? ret : null
})
-const isFileProperty = (toolProp: JSONSchema7Definition) : boolean => {
+const isFileArrayProperty = (toolProp: JSONSchema7Definition) : boolean => {
const toolPropSchema = js7(toolProp)
return toolPropSchema?.type === 'array' && (js7(toolPropSchema?.items)?.$ref?.endsWith('/File') ?? false)
}
+const isFileProperty = (toolProp: JSONSchema7Definition) : boolean => {
+ return js7(toolProp)?.$ref?.endsWith('/File') ?? false
+}
+
const isEnumProperty = (toolProp: JSONSchema7Definition) : boolean => {
const toolPropSchema = js7(toolProp)!
return toolPropSchema.type === 'array' && js7(toolPropSchema.items)?.enum !== undefined
@@ -179,15 +183,23 @@ const saveToolConfig = async () => {
// avoid saving tool when requires files and none have been uploaded
if (savedConfig.value !== mutableConfig && !(Object.values(toolProperties.value).some(isFileProperty) && !savedConfig.value)) {
partialSave = true
- ret = await handleOAuthRequestsIn(async () => await api.configureAgentTool(props.toolConfig.agentId, ret), api)
+ ret = await handleOAuthRequestsIn(async () => {
+ // resetting this flag since we want to allow retry saving tool config to re open oauth popup, or allow cancelling oauth
+ // when oauth popup is closed or just ignored
+ saving.value = true
+ try {
+ return await api.configureAgentTool(props.toolConfig.agentId, ret)
+ } finally {
+ // here is where we re enable save and cancel buttons in case oauth popup is showed or auth completed
+ saving.value = false
+ }
+ }, api)
partialSave = false
}
emit('update', ret)
} catch (error) {
- if (error instanceof AuthenticationWindowCloseError) {
- validationErrors.value = t('authenticationWindowClosed')
- } else if (error instanceof AuthenticationCancelError) {
- validationErrors.value = t('authenticationCancelled')
+ if (error instanceof AuthenticationError) {
+ validationErrors.value = t(error.errorCode)
} else if (error instanceof ValidationErrors) {
validationErrors.value = error.message
} else if (error instanceof HttpError && error.status === 400) {
@@ -259,20 +271,43 @@ class ValidationErrors extends Error {
-
-
-
-
-
-
+
+
diff --git a/src/frontend/src/components/agent/EvaluatorConfigurationModal.vue b/src/frontend/src/components/agent/EvaluatorConfigurationModal.vue
new file mode 100644
index 0000000..4252dd4
--- /dev/null
+++ b/src/frontend/src/components/agent/EvaluatorConfigurationModal.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
{{ testCaseId && testCaseName ? `${testCaseName} ${t('evaluator')}` : `${t('evaluatorTitle')}` }}
+
+
+
+
{{ t('testCaseEvaluatorNote') }}
+
+
+
+
+
+
+
+ {{ t('cancel') }}
+
+
+ {{ t('confirm') }}
+
+
+
+
+
+
+
+{
+ "en": {
+ "evaluator": "evaluator",
+ "evaluatorTitle": "Agent evaluator",
+ "testCaseEvaluatorNote": "This evaluator configuration will only be used for this specific test case.",
+ "modelLabel": "Model",
+ "instructionsLabel": "Instructions",
+ "instructionsPlaceholder": "Write the instructions for this evaluator",
+ "availableVariablesNote": "You can use these variables in your instructions:\n• {'{'}{'{'}inputs{'}'}{'}'} - user message\n• {'{'}{'{'}reference_outputs{'}'}{'}'} - expected response\n• {'{'}{'{'}outputs{'}'}{'}'} - actual agent response",
+ "cancel": "Cancel",
+ "confirm": "Confirm"
+
+ },
+ "es": {
+ "evaluator": "evaluador",
+ "evaluatorTitle": "Evaluador del agente",
+ "testCaseEvaluatorNote": "Esta configuración del evaluador solo se usará para este caso de prueba específico.",
+ "modelLabel": "Modelo",
+ "instructionsLabel": "Instrucciones",
+ "instructionsPlaceholder": "Escribe las instrucciones para este evaluador",
+ "availableVariablesNote": "Puedes usar estas variables en tus instrucciones:\n• {'{'}{'{'}inputs{'}'}{'}'} - mensaje del usuario\n• {'{'}{'{'}reference_outputs{'}'}{'}'} - respuesta esperada\n• {'{'}{'{'}outputs{'}'}{'}'} - respuesta actual del agente",
+ "cancel": "Cancelar",
+ "confirm": "Confirmar"
+
+ }
+}
+
diff --git a/src/frontend/src/components/agent/LlmModelSettings.vue b/src/frontend/src/components/agent/LlmModelSettings.vue
new file mode 100644
index 0000000..795e5e4
--- /dev/null
+++ b/src/frontend/src/components/agent/LlmModelSettings.vue
@@ -0,0 +1,92 @@
+
+
+
+
+ {{ t('temperatureLabel') }}
+
+
+
+ {{ t('reasoningEffortLabel') }}
+
+
+
+
+
+{
+ "en": {
+ "temperatureLabel": "Temperature",
+ "reasoningEffortLabel": "Reasoning",
+ "preciseTemperature": "Precise",
+ "neutralTemperature": "Neutral",
+ "creativeTemperature": "Creative",
+ "lowEffort": "Low",
+ "mediumEffort": "Medium",
+ "highEffort": "High"
+ },
+ "es": {
+ "temperatureLabel": "Temperatura",
+ "reasoningEffortLabel": "Razonamiento",
+ "preciseTemperature": "Preciso",
+ "neutralTemperature": "Neutro",
+ "creativeTemperature": "Creativo",
+ "lowEffort": "Bajo",
+ "mediumEffort": "Medio",
+ "highEffort": "Alto"
+ }
+}
+
diff --git a/src/frontend/src/components/chat/ChatPanel.vue b/src/frontend/src/components/chat/ChatPanel.vue
index 8278894..18615e5 100644
--- a/src/frontend/src/components/chat/ChatPanel.vue
+++ b/src/frontend/src/components/chat/ChatPanel.vue
@@ -1,7 +1,7 @@
diff --git a/src/frontend/src/components/common/NotificationItem.vue b/src/frontend/src/components/common/NotificationItem.vue
index 2ad27fa..555cdb8 100644
--- a/src/frontend/src/components/common/NotificationItem.vue
+++ b/src/frontend/src/components/common/NotificationItem.vue
@@ -55,7 +55,7 @@ const collapsed = ref(true);
"reject": "Reject",
"invitationText": "You have been invited to join the team",
"acceptText": "By accepting, the leaders of this team will be able to",
- "usageMetricsText": "See your usage metrics in AI Console.",
+ "usageMetricsText": "See your metrics of hours saved with AI and the number of chats.",
"editAgentsText": "Edit the agents you publish in this new team.",
"acceptButton": "Accept invitation",
"rejectButton": "Reject"
@@ -65,7 +65,7 @@ const collapsed = ref(true);
"reject": "Rechazar",
"invitationText": "Has sido invitado para unirte al equipo",
"acceptText": "Al aceptar, los lideres de este equipo podrán",
- "usageMetricsText": "Ver tus horas IA utilizadas en la consola de IA.",
+ "usageMetricsText": "Ver las métricas de tus horas ahorradas con IA y el número de chats.",
"editAgentsText": "Editar los agentes que publiques en este nuevo equipo.",
"acceptButton": "Aceptar invitación",
"rejectButton": "Rechazar"
diff --git a/src/frontend/src/components/agent/AgentTestcaseMenu.vue b/src/frontend/src/components/common/SimpleMenu.vue
similarity index 77%
rename from src/frontend/src/components/agent/AgentTestcaseMenu.vue
rename to src/frontend/src/components/common/SimpleMenu.vue
index 103b8ba..063f87b 100644
--- a/src/frontend/src/components/agent/AgentTestcaseMenu.vue
+++ b/src/frontend/src/components/common/SimpleMenu.vue
@@ -1,17 +1,12 @@
-
-
\ No newline at end of file
+
+
diff --git a/src/frontend/src/components/dashboard/DashboardCardUsers.vue b/src/frontend/src/components/dashboard/DashboardCardUsers.vue
index a284b41..b7b0a9f 100644
--- a/src/frontend/src/components/dashboard/DashboardCardUsers.vue
+++ b/src/frontend/src/components/dashboard/DashboardCardUsers.vue
@@ -35,7 +35,8 @@ const isSearchingUser = ref
(false)
const searchUser = ref('')
const roleNames = computed>(() => ({
[Role.TEAM_OWNER]: t('teamOwner'),
- [Role.TEAM_MEMBER]: t('teamMember')
+ [Role.TEAM_MEMBER]: t('teamMember'),
+ [Role.TEAM_EDITOR]: t('teamEditor')
}))
const showAddUserModal = ref(false)
@@ -251,6 +252,7 @@ const handleConfirmDeleteUser = async () => {
"noUsersFound": "No users found",
"teamOwner": "Leader",
"teamMember": "Member",
+ "teamEditor": "Editor",
"addUser": "Add",
"cancel": "Cancel",
"delete": "Remove",
@@ -272,6 +274,7 @@ const handleConfirmDeleteUser = async () => {
"noUsersFound": "No se encontraron usuarios",
"teamOwner": "Líder",
"teamMember": "Miembro",
+ "teamEditor": "Editor",
"addUser": "Agregar",
"cancel": "Cancelar",
"delete": "Remover",
diff --git a/src/frontend/src/components/dashboard/DashboardImpactCardTopAgents.vue b/src/frontend/src/components/dashboard/DashboardImpactCardTopAgents.vue
index 3891650..716e0b7 100644
--- a/src/frontend/src/components/dashboard/DashboardImpactCardTopAgents.vue
+++ b/src/frontend/src/components/dashboard/DashboardImpactCardTopAgents.vue
@@ -302,7 +302,8 @@ watch(() => props.teamId, async () => {
-{
+
+{
"en": {
"agentsTitle": "Agents",
"loadingMoreAgents": "Loading more agents...",
@@ -335,4 +336,5 @@ watch(() => props.teamId, async () => {
"startChatButtonLabel": "Usar ahora",
"viewDetailsTooltip": "Ver detalles"
}
-}
+}
+
diff --git a/src/frontend/src/components/discover/DiscoverTabs.vue b/src/frontend/src/components/discover/DiscoverTabs.vue
index 25785f6..879aa55 100644
--- a/src/frontend/src/components/discover/DiscoverTabs.vue
+++ b/src/frontend/src/components/discover/DiscoverTabs.vue
@@ -381,6 +381,9 @@ onMounted(async () => {
{{ isSearching ? t('searchingAgents') : t('noAgentsFound') }}
{{ isSearching ? t('searchingAgentsDescription') : t('noAgentsFoundDescription') }}
+
+
{{ t('noAgentsCreated') }}
+
@@ -431,7 +434,8 @@ onMounted(async () => {
"searchingAgents": "Searching agents",
"searchingAgentsDescription": "We are searching agents that match your search",
"noAgentsFound": "No agents found",
- "noAgentsFoundDescription": "We couldn't find any agents that match your search. Please try a different search term"
+ "noAgentsFoundDescription": "We couldn't find any agents that match your search. Please try a different search term",
+ "noAgentsCreated": "You haven't created agents"
},
"es": {
"discoverAgents": "A descubrir",
@@ -447,7 +451,8 @@ onMounted(async () => {
"searchingAgents": "Buscando agentes",
"searchingAgentsDescription": "Estamos buscando agentes que coincidan con tu búsqueda",
"noAgentsFound": "No se encontraron agentes",
- "noAgentsFoundDescription": "No pudimos encontrar agentes que coincidan con tu búsqueda. Por favor, intenta con otro término de búsqueda"
+ "noAgentsFoundDescription": "No pudimos encontrar agentes que coincidan con tu búsqueda. Por favor, intenta con otro término de búsqueda",
+ "noAgentsCreated": "No has creado agentes"
}
}
diff --git a/src/frontend/src/components/sidebar/SidebarPanel.vue b/src/frontend/src/components/sidebar/SidebarPanel.vue
index 9c4d1a0..b110c75 100644
--- a/src/frontend/src/components/sidebar/SidebarPanel.vue
+++ b/src/frontend/src/components/sidebar/SidebarPanel.vue
@@ -1,5 +1,6 @@
@@ -109,30 +119,50 @@ onMounted(async () => {
-
-
- {{ t(filterModeIsActive ? 'noAgentsFound' : 'noAgents')}}
+
+
+
+
+
+ {{ t('agents') }}
+
+
+
+
+
+
+ {{ t(filterModeIsActive ? 'noAgentsFound' : 'noAgents')}}
+
-
{{ t('chats') }}
+
+
+
+
+ {{ t('chats') }}
+
-
- {{ t('newChat') }}
+
+ {{ t('newChat') }}
-
-
- {{ t(filterModeIsActive ? 'noChatsFound' : 'noChats') }}
+
+
+
+ {{ t(filterModeIsActive ? 'noChatsFound' : 'noChats') }}
+
+
@@ -154,6 +184,8 @@ onMounted(async () => {
{
"en": {
"createAgent": "Create agent",
+ "agents": "Agents",
+ "newChatTooltip": "Start new chat with {name}",
"chats": "My chats",
"logout": "Log out",
"noChats": "Nothing to show here yet",
@@ -167,6 +199,8 @@ onMounted(async () => {
},
"es": {
"createAgent": "Crear agente",
+ "agents": "Agentes",
+ "newChatTooltip": "Iniciar nuevo chat con {name}",
"chats": "Mis chats",
"logout": "Cerrar sesión",
"noChats": "No hay nada para mostrar",
diff --git a/src/frontend/src/components/user/UsersForm.vue b/src/frontend/src/components/user/UsersForm.vue
index 2fc9e13..70a624d 100644
--- a/src/frontend/src/components/user/UsersForm.vue
+++ b/src/frontend/src/components/user/UsersForm.vue
@@ -40,7 +40,8 @@ const filteredUsers = computed(() => {
const roleOptions = ref<{ label: string, value: string }[]>([
{ label: t('teamOwner'), value: Role.TEAM_OWNER.toString() },
- { label: t('teamMember'), value: Role.TEAM_MEMBER.toString() }
+ { label: t('teamMember'), value: Role.TEAM_MEMBER.toString() },
+ { label: t('teamEditor'), value: Role.TEAM_EDITOR.toString() }
])
const handleUserChange = (user: NewUserRow, index: number) => {
@@ -119,18 +120,18 @@ defineExpose({
@@ -143,7 +144,9 @@ defineExpose({
-
{
+
+
+{
"en": {
"userName": "User name",
"userNamePlaceholder": "Enter user name",
@@ -151,6 +154,7 @@ defineExpose({
"role": "Role",
"teamOwner": "Leader",
"teamMember": "Member",
+ "teamEditor": "Editor",
"invalidEmail": "Invalid email"
},
"es": {
@@ -160,6 +164,8 @@ defineExpose({
"role": "Rol",
"teamOwner": "Líder",
"teamMember": "Miembro",
+ "teamEditor": "Editor",
"invalidEmail": "Email inválido"
}
-}
\ No newline at end of file
+}
+
diff --git a/src/frontend/src/composables/useAgentPromptStore.ts b/src/frontend/src/composables/useAgentPromptStore.ts
index acdcfdc..5a95bba 100644
--- a/src/frontend/src/composables/useAgentPromptStore.ts
+++ b/src/frontend/src/composables/useAgentPromptStore.ts
@@ -37,7 +37,7 @@ export function useAgentPromptStore() {
agentsPromptStore.setPrompts(await api.findAgentPrompts(agentId))
}
- async function updatePrompt(agentId: number, promptId:number, prompt: AgentPromptUpdate) {
+ async function updatePrompt(agentId: number, promptId: number, prompt: AgentPromptUpdate) {
const updatedPrompt = await api.updateAgentPrompt(agentId, promptId, prompt)
agentsPromptStore.updatePrompt(updatedPrompt)
}
diff --git a/src/frontend/src/composables/useTestCaseStore.ts b/src/frontend/src/composables/useTestCaseStore.ts
new file mode 100644
index 0000000..30e5d75
--- /dev/null
+++ b/src/frontend/src/composables/useTestCaseStore.ts
@@ -0,0 +1,104 @@
+import { reactive } from 'vue'
+import { ApiService, TestCase } from '@/services/api'
+
+const testCasesStore = reactive({
+ testCases: [] as TestCase[],
+ selectedTestCase: undefined as TestCase | undefined,
+ async setTestCases(testCases: TestCase[]) {
+ this.testCases = testCases
+ this.selectedTestCase = testCases.find(tc => tc.thread.id === this.selectedTestCase?.thread.id)
+ },
+ addTestCase(testCase: TestCase) {
+ this.testCases.push(testCase)
+ },
+ removeTestCase(testCaseThreadId: number) {
+ this.testCases = this.testCases.filter((tc) => tc.thread.id !== testCaseThreadId)
+ if (this.selectedTestCase?.thread.id === testCaseThreadId) {
+ this.selectedTestCase = this.testCases.length > 0 ? this.testCases[0] : undefined
+ }
+ },
+ updateTestCase(testCaseThreadId: number, updatedTestCase: TestCase) {
+ const index = this.testCases.findIndex((tc) => tc.thread.id === testCaseThreadId)
+ if (index !== -1) {
+ this.testCases[index] = updatedTestCase
+ if (this.selectedTestCase?.thread.id === testCaseThreadId) {
+ this.selectedTestCase = updatedTestCase
+ }
+ }
+ },
+ setSelectedTestCase(testCase: TestCase | undefined) {
+ this.setSelectedTestCaseById(testCase?.thread.id)
+ },
+ setSelectedTestCaseById(testCaseId: number | undefined) {
+ this.selectedTestCase = testCaseId ? this.testCases.find(tc => tc.thread.id === testCaseId) : undefined
+ },
+ clearTestCases() {
+ this.testCases = []
+ this.selectedTestCase = undefined
+ }
+})
+
+export function useTestCaseStore() {
+ const api = new ApiService()
+
+ async function loadTestCases(agentId: number) {
+ testCasesStore.setTestCases(await api.findTestCases(agentId))
+ }
+
+ async function addTestCase(agentId: number) {
+ const testCase = await api.addTestCase(agentId)
+ if (testCasesStore.testCases.find(tc => tc.thread.id === testCase.thread.id)) {
+ testCasesStore.updateTestCase(testCase.thread.id, testCase)
+ } else {
+ testCasesStore.addTestCase(testCase)
+ }
+ return testCase
+ }
+
+ async function deleteTestCase(agentId: number, testCaseThreadId: number) {
+ await api.deleteTestCase(agentId, testCaseThreadId)
+ testCasesStore.removeTestCase(testCaseThreadId)
+ }
+
+ async function updateTestCase(agentId: number, testCaseThreadId: number, name: string) {
+ const updatedTestCase = await api.updateTestCase(agentId, testCaseThreadId, name)
+ testCasesStore.updateTestCase(testCaseThreadId, updatedTestCase)
+ }
+
+ async function refreshTestCase(agentId: number, testCaseThreadId: number) {
+ const testCase = await api.findTestCase(agentId, testCaseThreadId)
+ testCasesStore.updateTestCase(testCaseThreadId, testCase)
+ return testCase
+ }
+
+ async function cloneTestCase(agentId: number, testCaseThreadId: number) {
+ const clonedTestCase = await api.cloneTestCase(agentId, testCaseThreadId)
+ testCasesStore.addTestCase(clonedTestCase)
+ return clonedTestCase
+ }
+
+ function clearTestCases() {
+ testCasesStore.clearTestCases()
+ }
+
+ function setSelectedTestCase(testCase: TestCase | undefined) {
+ testCasesStore.setSelectedTestCase(testCase)
+ }
+
+ function setSelectedTestCaseById(testCaseId: number | undefined) {
+ testCasesStore.setSelectedTestCaseById(testCaseId)
+ }
+
+ return {
+ testCasesStore,
+ loadTestCases,
+ addTestCase,
+ deleteTestCase,
+ updateTestCase,
+ refreshTestCase,
+ cloneTestCase,
+ clearTestCases,
+ setSelectedTestCase,
+ setSelectedTestCaseById
+ }
+}
diff --git a/src/frontend/src/composables/useTestExecutionStore.ts b/src/frontend/src/composables/useTestExecutionStore.ts
new file mode 100644
index 0000000..53cd8aa
--- /dev/null
+++ b/src/frontend/src/composables/useTestExecutionStore.ts
@@ -0,0 +1,291 @@
+import { reactive } from 'vue'
+import { ApiService, TestCase, TestCaseResult, TestCaseResultStatus, TestSuiteRun, TestSuiteRunStatus } from '@/services/api'
+import type { TestSuiteExecutionStreamEvent } from '@/services/api'
+
+export interface TestCaseExecutionState {
+ phase: string;
+ userMessage?: { id: number; text: string };
+ agentMessage?: { id: number; text: string, complete: boolean };
+ status?: TestCaseResultStatus;
+ statusUpdates?: any[];
+}
+
+const testExecutionStore = reactive({
+ selectedSuiteRun: undefined as TestSuiteRun | undefined,
+ selectedResult: undefined as TestCaseResult | undefined,
+ testCaseResults: [] as TestCaseResult[],
+ executionStates: new Map
(),
+ isStoppingSuite: false,
+
+ setSelectedSuiteRun(suiteRun: TestSuiteRun | undefined) {
+ this.selectedSuiteRun = suiteRun
+ },
+
+ setSelectedResult(result: TestCaseResult | undefined) {
+ this.selectedResult = result
+ },
+
+ setTestCaseResults(results: TestCaseResult[]) {
+ this.testCaseResults = results
+ this.selectedResult = results.find(r => r.testCaseId === this.selectedResult?.testCaseId)
+ },
+
+ setExecutionState(testCaseResultId: number, state: TestCaseExecutionState) {
+ this.executionStates.set(testCaseResultId, state)
+ },
+
+ getExecutionState(testCaseResultId: number): TestCaseExecutionState | undefined {
+ return this.executionStates.get(testCaseResultId)
+ },
+
+ deleteExecutionState(testCaseResultId: number) {
+ this.executionStates.delete(testCaseResultId)
+ },
+
+ clearExecutionStates() {
+ this.executionStates.clear()
+ },
+
+ setTestCaseResultStatus(testCaseId: number, status: TestCaseResultStatus) {
+ const result = this.testCaseResults.find(tr => tr.testCaseId === testCaseId)
+ if (!result) {
+ return
+ }
+
+ result.status = status
+
+ if (this.selectedResult?.testCaseId === testCaseId) {
+ this.selectedResult.status = status
+ }
+ },
+
+ clear() {
+ this.selectedSuiteRun = undefined
+ this.selectedResult = undefined
+ this.testCaseResults = []
+ this.executionStates.clear()
+ }
+})
+
+export function useTestExecutionStore() {
+ const api = new ApiService()
+
+ async function loadSuiteRunResults(agentId: number, suiteRunId: number) {
+ const results = await api.findTestSuiteRunResults(agentId, suiteRunId)
+ testExecutionStore.setTestCaseResults(results)
+ return results
+ }
+
+ function processStreamEvent(
+ event: TestSuiteExecutionStreamEvent,
+ options?: {
+ currentTestCaseResultId?: number
+ agentId?: number
+ }
+ ): number | undefined {
+ let currentTestCaseResultId: number | undefined = options?.currentTestCaseResultId
+
+ switch (event.type) {
+ case 'suite.test.start':
+ const testCaseId = event.data.testCaseId;
+ currentTestCaseResultId = event.data.resultId;
+
+ const testCaseResult = testExecutionStore.testCaseResults.find(tr => tr.testCaseId === testCaseId);
+ if (testCaseResult) {
+ testExecutionStore.setTestCaseResultStatus(testCaseId, TestCaseResultStatus.RUNNING);
+ testCaseResult.id = currentTestCaseResultId;
+ }
+
+ testExecutionStore.setExecutionState(currentTestCaseResultId, {
+ phase: 'executing',
+ statusUpdates: []
+ });
+
+ break;
+
+ case 'suite.test.metadata':
+ const testResult = testExecutionStore.testCaseResults.find(tr => tr.testCaseId === event.data.testCaseId);
+ if (testResult) {
+ testResult.id = event.data.resultId;
+ if (testExecutionStore.selectedSuiteRun) {
+ testResult.testSuiteRunId = testExecutionStore.selectedSuiteRun.id;
+ }
+ }
+ break;
+
+ case 'suite.test.phase':
+ const execState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (execState) {
+ execState.phase = event.data.phase;
+ if (event.data.phase === 'completed') {
+ execState.status = event.data.status as TestCaseResultStatus;
+ }
+ }
+ break;
+
+ case 'suite.test.userMessage':
+ const userExecState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (userExecState) {
+ userExecState.userMessage = {
+ id: event.data.id,
+ text: event.data.text
+ };
+ }
+ break;
+
+ case 'suite.test.agentMessage.start':
+ const startExecState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (startExecState) {
+ startExecState.agentMessage = {
+ id: event.data.id,
+ text: '',
+ complete: false
+ };
+ }
+ break;
+
+ case 'suite.test.agentMessage.chunk':
+ const chunkExecState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (chunkExecState && chunkExecState.agentMessage) {
+ chunkExecState.agentMessage.text += event.data.chunk;
+ }
+ break;
+
+ case 'suite.test.agentMessage.complete':
+ const completeExecState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (completeExecState && completeExecState.agentMessage) {
+ completeExecState.agentMessage.text = event.data.text;
+ completeExecState.agentMessage.complete = true;
+ }
+ break;
+
+ case 'suite.test.executionStatus':
+ const statusExecState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (statusExecState) {
+ statusExecState.statusUpdates!.push(event.data);
+ }
+ break;
+
+ case 'suite.test.error':
+ const errorExecState = testExecutionStore.getExecutionState(currentTestCaseResultId!);
+ if (errorExecState) {
+ errorExecState.phase = 'completed';
+ errorExecState.status = TestCaseResultStatus.ERROR;
+ }
+
+ const errorResult = testExecutionStore.testCaseResults.find(tr => tr.id === currentTestCaseResultId!);
+ if (errorResult) {
+ testExecutionStore.setTestCaseResultStatus(errorResult.testCaseId, TestCaseResultStatus.ERROR);
+ }
+ break;
+
+ case 'suite.test.complete':
+ const completedTestCaseId = event.data.testCaseId;
+ updateTestCaseResult(completedTestCaseId, event.data.status as TestCaseResultStatus, event.data.evaluation?.analysis);
+
+ const completedResult = testExecutionStore.testCaseResults.find(tr => tr.testCaseId === completedTestCaseId);
+ if (completedResult?.id) {
+ testExecutionStore.deleteExecutionState(completedResult.id);
+ }
+ break;
+
+ case 'suite.error':
+ testExecutionStore.testCaseResults.forEach(result => {
+ if (result.status === TestCaseResultStatus.RUNNING || result.status === TestCaseResultStatus.PENDING) {
+ testExecutionStore.setTestCaseResultStatus(result.testCaseId, TestCaseResultStatus.SKIPPED);
+ }
+ });
+
+ testExecutionStore.clearExecutionStates();
+ break;
+
+ case 'suite.complete':
+ if (testExecutionStore.selectedSuiteRun) {
+ testExecutionStore.selectedSuiteRun.status = event.data.status as TestSuiteRunStatus
+ testExecutionStore.selectedSuiteRun.completedAt = new Date()
+ testExecutionStore.selectedSuiteRun.passedTests = event.data.passed
+ testExecutionStore.selectedSuiteRun.failedTests = event.data.failed
+ testExecutionStore.selectedSuiteRun.errorTests = event.data.errors
+ testExecutionStore.selectedSuiteRun.skippedTests = event.data.skipped
+ }
+ break;
+ }
+
+ return currentTestCaseResultId;
+ }
+
+ function updateTestCaseResult(testCaseId: number, status: TestCaseResultStatus, evaluatorAnalysis?: string) {
+ const result = testExecutionStore.testCaseResults.find(tr => tr.testCaseId === testCaseId)
+ if (result) {
+ result.status = status
+ result.evaluatorAnalysis = evaluatorAnalysis
+ if (testExecutionStore.selectedResult?.testCaseId === testCaseId) {
+ testExecutionStore.selectedResult = result
+ }
+ }
+ }
+
+ function initializeTestRun(agentId: number, testCases: TestCase[], singleTestCaseId?: number) {
+ const results = testCases.map(testCase => {
+ return new TestCaseResult(
+ testCase.thread.id,
+ new Date(),
+ singleTestCaseId ? (testCase.thread.id === singleTestCaseId ? TestCaseResultStatus.PENDING : TestCaseResultStatus.SKIPPED) : TestCaseResultStatus.PENDING,
+ testCase.thread.name
+ )
+ })
+ testExecutionStore.setTestCaseResults(results)
+ setSelectedResult(singleTestCaseId ? results.find(result => result.testCaseId === singleTestCaseId)! : results[0]);
+
+ testExecutionStore.setSelectedSuiteRun({
+ id: 0,
+ agentId: agentId,
+ status: TestSuiteRunStatus.RUNNING,
+ executedAt: new Date(),
+ totalTests: testCases.length,
+ passedTests: 0,
+ failedTests: 0,
+ errorTests: 0,
+ skippedTests: 0
+ })
+ }
+
+ function setSelectedResult(result: TestCaseResult) {
+ testExecutionStore.setSelectedResult(result)
+ }
+
+ async function stopSuiteRun() {
+ const selectedSuiteRun = testExecutionStore.selectedSuiteRun
+ if (!selectedSuiteRun || testExecutionStore.isStoppingSuite) {
+ return
+ }
+
+ testExecutionStore.isStoppingSuite = true
+
+ try {
+ await api.stopTestSuiteRun(selectedSuiteRun.agentId, selectedSuiteRun.id)
+ testExecutionStore.testCaseResults.forEach(result => {
+ if (result.status === TestCaseResultStatus.RUNNING || result.status === TestCaseResultStatus.PENDING) {
+ testExecutionStore.setTestCaseResultStatus(result.testCaseId, TestCaseResultStatus.SKIPPED)
+ }
+ })
+ } finally {
+ testExecutionStore.isStoppingSuite = false
+ }
+ }
+
+ function clear() {
+ testExecutionStore.clear()
+ }
+
+ return {
+ testExecutionStore,
+ loadSuiteRunResults,
+ processStreamEvent,
+ updateTestCaseResult,
+ initializeTestRun,
+ stopSuiteRun,
+ setSelectedResult,
+ clear,
+ }
+}
diff --git a/src/frontend/src/pages/AgentEditorPage.vue b/src/frontend/src/pages/AgentEditorPage.vue
index 130e3cd..0a29627 100644
--- a/src/frontend/src/pages/AgentEditorPage.vue
+++ b/src/frontend/src/pages/AgentEditorPage.vue
@@ -2,42 +2,34 @@
import { onMounted, onBeforeUnmount, ref } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import { useI18n } from 'vue-i18n';
-import { ApiService, Thread, HttpError, TestCase, TestCaseResult, TestCaseResultStatus, TestSuiteRun, TestSuiteRunStatus } from '@/services/api';
+import { ApiService, Thread, HttpError, TestCaseResultStatus, TestSuiteRun, TestSuiteRunStatus } from '@/services/api';
import type { TestSuiteExecutionStreamEvent } from '@/services/api';
import AgentTestcasePanel from '@/components/agent/AgentTestcasePanel.vue';
import { useErrorHandler } from '@/composables/useErrorHandler';
import { useToast } from 'vue-toastification';
import ToastMessage from '@/components/common/ToastMessage.vue';
+import { useTestCaseStore } from '@/composables/useTestCaseStore';
+import { useTestExecutionStore } from '@/composables/useTestExecutionStore';
+import { handleOAuthRequestsIn, AuthenticationError } from '@/services/toolOAuth';
-export interface TestCaseExecutionState {
- phase: string;
- userMessage?: { id: number; text: string };
- agentMessage?: { id: number; text: string };
- agentChunks: string;
- status?: TestCaseResultStatus;
- statusUpdates: any[];
-}
+export type { TestCaseExecutionState } from '@/composables/useTestExecutionStore';
const { t } = useI18n()
const { handleError } = useErrorHandler()
const toast = useToast()
+const { testCasesStore, loadTestCases: loadTestCasesFromStore, clearTestCases } = useTestCaseStore()
+const { testExecutionStore, loadSuiteRunResults, processStreamEvent, initializeTestRun, clear: clearExecutionStore } = useTestExecutionStore()
const api = new ApiService();
const route = useRoute();
const router = useRouter();
const agentId = ref();
const threadId = ref();
const showTestCaseEditor = ref(false);
-const testCaseId = ref();
-const isEditingTestCase = ref(false);
-const testCases = ref([]);
-const testCaseResults = ref([]);
-const latestSuiteRun = ref();
+const isEditingTestCase = ref(true);
const testCasePanel = ref>();
const loadingTests = ref(true);
-const runningTests = ref(false);
-const executionStates = ref>(new Map());
const testRunStartedByCurrentUser = ref(false);
-let pollInterval: number | null = null;
+const isComparingResultWithTestSpec = ref(false);
const startChat = async () => {
try {
@@ -48,345 +40,109 @@ const startChat = async () => {
router.push('/not-found');
}
}
-};
+}
const handleSelectChat = (chat: Thread) => {
threadId.value = chat.id;
}
-const handleSelectTestCase = (id: number | undefined) => {
- testCaseId.value = id;
-}
-
-const handleNewTestCase = (testCase: TestCase) => {
- testCaseId.value = testCase.thread.id;
- testCases.value.push(testCase);
-}
-
-const loadTestCases = async (id: number) => {
+const handleSelectExecution = async (execution: TestSuiteRun) => {
+ testExecutionStore.clear()
+ testExecutionStore.setSelectedSuiteRun(execution)
try {
- testCases.value = await api.findTestCases(id)
- const suiteRuns = await api.findTestSuiteRuns(id, 1, 0)
- latestSuiteRun.value = suiteRuns.length > 0 ? suiteRuns[0] : undefined
- testCaseResults.value = latestSuiteRun.value ? await api.findTestSuiteRunResults(id, latestSuiteRun.value.id) : []
-
- if (!testCases.value.length) {
- isEditingTestCase.value = true
- }
- else if (testCaseResults.value.length > 0) {
- handleSelectTestCase(testCases.value[0].thread.id)
- isEditingTestCase.value = false
- }
-
- if (latestSuiteRun.value && latestSuiteRun.value.status === TestSuiteRunStatus.RUNNING) {
- runningTests.value = true
- startPolling()
- }
-
- } catch (e) {
- handleError(e)
- } finally {
- loadingTests.value = false
- }
-}
-
-const handleDeleteTestCase = (testCaseThreadId: number) => {
- const isSelectedTestCase = testCaseThreadId === testCaseId.value
- testCases.value = testCases.value.filter(tc => tc.thread.id !== testCaseThreadId)
- if (isSelectedTestCase) {
- handleSelectTestCase(undefined)
- }
- if (testCases.value.length === 0 && !isEditingTestCase.value) {
- isEditingTestCase.value = true
- }
-}
-
-const startPolling = () => {
- stopPolling()
-
- pollInterval = window.setInterval(async () => {
- await pollTestSuiteStatus()
- }, 1000)
-}
-
-const stopPolling = () => {
- if (pollInterval !== null) {
- window.clearInterval(pollInterval)
- pollInterval = null
- }
-}
-
-const pollTestSuiteStatus = async () => {
- if (!agentId.value) return
-
- try {
- const suiteRuns = await api.findTestSuiteRuns(agentId.value, 1, 0)
-
- if (suiteRuns.length === 0) {
- stopPolling()
- runningTests.value = false
- return
- }
-
- latestSuiteRun.value = suiteRuns[0]
-
- const previousSelectedResult = testCaseId.value
- ? testCaseResults.value.find(tr => tr.testCaseId === testCaseId.value)
- : undefined
-
- testCaseResults.value = await api.findTestSuiteRunResults(agentId.value, latestSuiteRun.value!.id)
-
- const currentSelectedResult = testCaseId.value
- ? testCaseResults.value.find(tr => tr.testCaseId === testCaseId.value)
- : undefined
-
- if (testCaseId.value && currentSelectedResult && previousSelectedResult) {
- const statusChanged = previousSelectedResult.status !== currentSelectedResult.status
- const isNoLongerRunning = currentSelectedResult.status !== TestCaseResultStatus.RUNNING
-
- if (statusChanged && isNoLongerRunning) {
- await testCasePanel.value?.loadTestCaseData()
- }
- }
-
- if (latestSuiteRun.value!.status !== TestSuiteRunStatus.RUNNING) {
- stopPolling()
- runningTests.value = false
- testRunStartedByCurrentUser.value = false
- }
- } catch (e) {
- handleError(e)
- stopPolling()
- runningTests.value = false
- testRunStartedByCurrentUser.value = false
+ const results = await loadSuiteRunResults(agentId.value!, execution.id)
+ testExecutionStore.setSelectedResult(results[0])
+ } catch (error) {
+ handleError(error)
}
+ isEditingTestCase.value = false
}
const processSuiteExecutionStream = async (
eventStream: AsyncIterable,
- options?: {
- onTestStart?: (testCaseId: number) => void
- }
) => {
- let currentTestCaseId: number | null = null;
- let is409Error = false;
-
+ let currentTestCaseResultId: number | undefined = undefined;
try {
for await (const event of eventStream) {
- switch (event.type) {
- case 'suite.start':
- latestSuiteRun.value = {
- id: event.data.suiteRunId,
- agentId: agentId.value!,
- status: TestSuiteRunStatus.RUNNING,
- executedAt: new Date(),
- totalTests: testCases.value.length,
- passedTests: 0,
- failedTests: 0,
- errorTests: 0,
- skippedTests: 0
- }
- break;
-
- case 'suite.test.start':
- currentTestCaseId = event.data.testCaseId;
-
- const testCaseResult = testCaseResults.value.find(tr => tr.testCaseId === currentTestCaseId);
- if (testCaseResult) {
- testCaseResult.status = TestCaseResultStatus.RUNNING;
- testCaseResult.id = event.data.resultId;
- }
-
- executionStates.value.set(currentTestCaseId, {
- phase: 'executing',
- agentChunks: '',
- statusUpdates: []
- });
-
- if (options?.onTestStart) {
- options.onTestStart(currentTestCaseId);
- }
- break;
-
- case 'suite.test.metadata':
- const testResult = testCaseResults.value.find(tr => tr.testCaseId === event.data.testCaseId);
- if (testResult) {
- testResult.id = event.data.resultId;
- if (latestSuiteRun.value) {
- testResult.testSuiteRunId = latestSuiteRun.value.id;
- }
- }
- break;
-
- case 'suite.test.phase':
- const execState = currentTestCaseId ? executionStates.value.get(currentTestCaseId) : null;
- if (execState) {
- execState.phase = event.data.phase;
- if (event.data.phase === 'completed') {
- execState.status = event.data.status as TestCaseResultStatus;
- }
- }
- break;
-
- case 'suite.test.userMessage':
- const userExecState = currentTestCaseId ? executionStates.value.get(currentTestCaseId) : null;
- if (userExecState) {
- userExecState.userMessage = {
- id: event.data.id,
- text: event.data.text
- }
- }
- break;
-
- case 'suite.test.agentMessage.start':
- const startExecState = currentTestCaseId ? executionStates.value.get(currentTestCaseId) : null;
- if (startExecState) {
- startExecState.agentMessage = {
- id: event.data.id,
- text: ''
- };
- startExecState.agentChunks = '';
- }
- break;
-
- case 'suite.test.agentMessage.chunk':
- const chunkExecState = currentTestCaseId ? executionStates.value.get(currentTestCaseId) : null;
- if (chunkExecState) {
- chunkExecState.agentChunks += event.data.chunk;
- }
- break;
-
- case 'suite.test.agentMessage.complete':
- const completeExecState = currentTestCaseId ? executionStates.value.get(currentTestCaseId) : null;
- if (completeExecState && completeExecState.agentMessage) {
- completeExecState.agentMessage.text = event.data.text;
- }
- break;
-
- case 'suite.test.executionStatus':
- const statusExecState = currentTestCaseId ? executionStates.value.get(currentTestCaseId) : null;
- if (statusExecState) {
- statusExecState.statusUpdates.push(event.data);
- }
- break;
-
- case 'suite.test.error':
- if (currentTestCaseId) {
- const errorExecState = executionStates.value.get(currentTestCaseId);
- if (errorExecState) {
- errorExecState.phase = 'completed';
- errorExecState.status = TestCaseResultStatus.ERROR;
- }
- handleTestCaseCompleted(currentTestCaseId, TestCaseResultStatus.ERROR);
- }
- break;
-
- case 'suite.test.complete':
- const completedTestCaseId = event.data.testCaseId;
- handleTestCaseCompleted(completedTestCaseId, event.data.status as TestCaseResultStatus);
- executionStates.value.delete(completedTestCaseId);
- break;
-
- case 'suite.error':
- toast.error(
- { component: ToastMessage, props: { message: t('suiteExecutionFailed') } },
- { timeout: 5000, icon: false }
- );
-
- testCaseResults.value.forEach(result => {
- if (result.status === TestCaseResultStatus.RUNNING || result.status === TestCaseResultStatus.PENDING) {
- result.status = TestCaseResultStatus.SKIPPED;
- }
- });
-
- executionStates.value = new Map();
- break;
-
- case 'suite.complete':
- if (latestSuiteRun.value) {
- latestSuiteRun.value.status = event.data.status as TestSuiteRunStatus
- latestSuiteRun.value.completedAt = new Date()
- latestSuiteRun.value.passedTests = event.data.passed
- latestSuiteRun.value.failedTests = event.data.failed
- latestSuiteRun.value.errorTests = event.data.errors
- latestSuiteRun.value.skippedTests = event.data.skipped
- }
- break;
+ const updatedResultId = processStreamEvent(event, {
+ agentId: agentId.value,
+ currentTestCaseResultId
+ });
+ currentTestCaseResultId = updatedResultId ?? currentTestCaseResultId;
+
+ if (event.type === 'suite.error') {
+ toast.error(
+ { component: ToastMessage, props: { message: t('suiteExecutionFailed') } },
+ { timeout: 5000, icon: false }
+ );
}
}
} catch (error) {
- if (error instanceof HttpError && error.status === 409) {
- is409Error = true;
- toast.info(
- { component: ToastMessage, props: { message: t('suiteAlreadyRunning') } },
- { timeout: 5000, icon: false }
- )
- testRunStartedByCurrentUser.value = false
- startPolling()
- } else {
- if (currentTestCaseId) {
- const testCaseResult = testCaseResults.value.find(tr => tr.testCaseId === currentTestCaseId);
- if (testCaseResult) {
- testCaseResult.status = TestCaseResultStatus.ERROR;
- }
- const execState = executionStates.value.get(currentTestCaseId);
- if (execState) {
- execState.phase = 'completed';
- execState.status = TestCaseResultStatus.ERROR;
- }
+ if (currentTestCaseResultId) {
+ const testCaseResult = testExecutionStore.testCaseResults.find(tr => tr.id === currentTestCaseResultId);
+ if (testCaseResult) {
+ testCaseResult.status = TestCaseResultStatus.ERROR;
}
- handleError(error)
+ testExecutionStore.setExecutionState(currentTestCaseResultId, {
+ phase: 'completed',
+ status: TestCaseResultStatus.ERROR
+ });
}
+ handleError(error)
} finally {
- executionStates.value.clear();
- if (!is409Error) {
- runningTests.value = false;
- testRunStartedByCurrentUser.value = false;
- }
+ testExecutionStore.clearExecutionStates();
+ testRunStartedByCurrentUser.value = false;
}
}
-const handleRunTests = async () => {
+const runTestSuite = async (testCaseIds?: number[]) => {
isEditingTestCase.value = false
- runningTests.value = true
testRunStartedByCurrentUser.value = true
- testCaseResults.value = testCases.value.map(testCase => {
- return new TestCaseResult(
- testCase.thread.id,
- new Date(),
- TestCaseResultStatus.PENDING
+ try {
+ initializeTestRun(agentId.value!, testCasesStore.testCases, testCaseIds?.[0])
+ const suiteRun = await handleOAuthRequestsIn(
+ () => api.runTestSuite(agentId.value!, testCaseIds),
+ api
)
- })
-
- await processSuiteExecutionStream(api.runTestSuiteStream(agentId.value!))
-}
-
-const handleTestCaseCompleted = (completedTestCaseId: number, status: TestCaseResultStatus) => {
- const result = testCaseResults.value.find(tr => tr.testCaseId === completedTestCaseId)
- if (result) {
- result.status = status
+ testExecutionStore.setSelectedSuiteRun(suiteRun)
+ await processSuiteExecutionStream(api.streamTestSuiteUpdates(agentId.value!, suiteRun.id))
+ } catch (error) {
+ if (error instanceof AuthenticationError) {
+ toast.error(
+ { component: ToastMessage, props: { message: t('authenticationCancelled') } },
+ { timeout: 5000, icon: false }
+ )
+ isEditingTestCase.value = true
+ testRunStartedByCurrentUser.value = false
+ return
+ }
+ await handleRunError(error)
}
}
-const handleRunSingleTest = async (singleTestCaseId: number) => {
- isEditingTestCase.value = false
- runningTests.value = true
- testRunStartedByCurrentUser.value = true
-
- testCaseResults.value = testCases.value.map(testCase => {
- return new TestCaseResult(
- testCase.thread.id,
- new Date(),
- testCase.thread.id === singleTestCaseId ? TestCaseResultStatus.PENDING : TestCaseResultStatus.SKIPPED
- )
- })
-
- await processSuiteExecutionStream(api.runTestSuiteStream(agentId.value!, [singleTestCaseId]), {
- onTestStart: (testCaseId) => {
- handleSelectTestCase(testCaseId)
+const handleRunError = async (error: unknown) => {
+ if (error instanceof HttpError && error.status === 409) {
+ try {
+ toast.info(
+ { component: ToastMessage, props: { message: t('suiteAlreadyRunning') } },
+ { timeout: 5000, icon: false }
+ )
+ testRunStartedByCurrentUser.value = false
+ const suiteRuns = await api.findTestSuiteRuns(agentId.value!, 1, 0)
+ if (suiteRuns.length && suiteRuns[0].status === TestSuiteRunStatus.RUNNING) {
+ testExecutionStore.setSelectedSuiteRun(suiteRuns[0])
+ const results = await loadSuiteRunResults(agentId.value!, suiteRuns[0].id)
+ testExecutionStore.setSelectedResult(results[0])
+ await processSuiteExecutionStream(api.streamTestSuiteUpdates(agentId.value!, suiteRuns[0].id))
+ }
+ } catch (error) {
+ handleError(error)
}
- })
+ } else {
+ handleError(error)
+ testRunStartedByCurrentUser.value = false
+ }
}
onMounted(async () => {
@@ -397,10 +153,38 @@ onMounted(async () => {
}
});
+const loadTestCases = async (id: number) => {
+ try {
+ await loadTestCasesFromStore(id)
+
+ if(testCasesStore.testCases.length) {
+ testCasesStore.setSelectedTestCase(testCasesStore.testCases[0])
+ }
+
+ const suiteRuns = await api.findTestSuiteRuns(id, 1, 0)
+ if (suiteRuns.length && suiteRuns[0].status === TestSuiteRunStatus.RUNNING) {
+ isEditingTestCase.value = false
+ testExecutionStore.setSelectedSuiteRun(suiteRuns[0])
+ const results = await loadSuiteRunResults(id, suiteRuns[0].id)
+ testExecutionStore.setSelectedResult(results[0])
+ processSuiteExecutionStream(api.streamTestSuiteUpdates(id, suiteRuns[0].id))
+ }
+ } catch (e) {
+ handleError(e)
+ } finally {
+ loadingTests.value = false
+ }
+}
+
+const onEditingTestCase = (editing: boolean) => {
+ if(testExecutionStore.selectedSuiteRun?.status == TestSuiteRunStatus.RUNNING) return
+ isEditingTestCase.value = editing
+}
+
onBeforeRouteUpdate(async (to) => {
- stopPolling();
- runningTests.value = false;
testRunStartedByCurrentUser.value = false;
+ clearTestCases();
+ clearExecutionStore();
agentId.value = parseInt(to.params.agentId as string);
await startChat();
@@ -408,43 +192,42 @@ onBeforeRouteUpdate(async (to) => {
await loadTestCases(agentId.value);
}
});
-
-onBeforeUnmount(() => {
- stopPolling();
-});
-
+ runTestSuite([id])" @select-execution="handleSelectExecution"
+ :is-comparing-result-with-test-spec="isComparingResultWithTestSpec"
+ :test-spec-messages="testCasePanel?.testSpecMessages"/>
-
-
+
+
-{
+
+{
"en": {
"suiteExecutionFailed": "Test suite execution failed",
- "suiteAlreadyRunning": "Please wait for the test suite to finish running before starting a new execution"
+ "suiteAlreadyRunning": "Please wait for the test suite to finish running before starting a new execution",
+ "authenticationCancelled": "Tool authentication was cancelled. Please authenticate to run tests."
},
"es": {
"suiteExecutionFailed": "Falló la ejecución de la suite de tests",
- "suiteAlreadyRunning": "Espera a que el test suite termine de correr para lanzar una nueva ejecucion"
+ "suiteAlreadyRunning": "Espera a que el test suite termine de correr para lanzar una nueva ejecucion",
+ "authenticationCancelled": "La autenticación de la herramienta fue cancelada. Por favor autentícate para ejecutar los tests."
}
-}
+}
+
diff --git a/src/frontend/src/pages/FilePreviewPage.vue b/src/frontend/src/pages/FilePreviewPage.vue
index d7aa9e0..268987a 100644
--- a/src/frontend/src/pages/FilePreviewPage.vue
+++ b/src/frontend/src/pages/FilePreviewPage.vue
@@ -129,7 +129,7 @@ const findFile = async () : Promise<[File, ProcessedContent]> => {
})()])
} else {
return await Promise.all([api.downloadAgentToolFile(parsedAgentId.value, toolId!, fileId), (async () => {
- const docFile = await api.findAgentDocToolFile(parsedAgentId.value, toolId!, fileId)
+ const docFile = await api.findAgentToolFile(parsedAgentId.value, toolId!, fileId)
return new ProcessedContent(docFile.status, docFile.fileProcessor, docFile.processedContent)
})()])
}
@@ -179,7 +179,7 @@ const reprocess = async () => {
const agentId = parsedAgentId.value
await api.configureAgentTool(agentId, new AgentToolConfig(toolId!, {advancedFileProcessing: newProcessor === FileProcessor.ENHANCED}))
await api.updateAgentToolFile(agentId, toolId!, fileId, new File([], originalFile.value!.name, { type: originalFile.value!.type }))
- let toolFile = await api.findAgentDocToolFile(agentId, toolId!, fileId)
+ let toolFile = await api.findAgentToolFile(agentId, toolId!, fileId)
toolFile = await awaitFileProcessingCompletes(toolFile)
processedContent.value = toolFile.processedContent
const quotaExceeded = await checkQuotaExceeded(toolFile)
@@ -194,7 +194,7 @@ const reprocess = async () => {
const awaitFileProcessingCompletes = async (toolFile: DocToolFile) => {
while (toolFile.status === FileStatus.PENDING) {
- toolFile = await api.findAgentDocToolFile(parsedAgentId.value, toolId!, fileId)
+ toolFile = await api.findAgentToolFile(parsedAgentId.value, toolId!, fileId)
if (toolFile.status === FileStatus.PENDING) {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
diff --git a/src/frontend/src/pages/ToolAuthPage.vue b/src/frontend/src/pages/ToolAuthPage.vue
index 5e30c12..59a4279 100644
--- a/src/frontend/src/pages/ToolAuthPage.vue
+++ b/src/frontend/src/pages/ToolAuthPage.vue
@@ -7,13 +7,20 @@ const route = useRoute();
const { t } = useI18n();
onBeforeMount(() => {
- const { code, state } = route.query;
- window.opener.postMessage({
+ const { state, error, code } = route.query;
+ const channel = new BroadcastChannel('oauth_channel');
+ try {
+ channel.postMessage({
type: 'oauth_callback',
toolId: route.params.toolId,
- code: code,
state: state,
- }, window.location.origin);
+ error: error,
+ code: code,
+ });
+ } finally {
+ channel.close();
+ window.close();
+ }
});
@@ -23,7 +30,7 @@ onBeforeMount(() => {
diff --git a/src/frontend/src/services/api.ts b/src/frontend/src/services/api.ts
index eadec11..01de60e 100644
--- a/src/frontend/src/services/api.ts
+++ b/src/frontend/src/services/api.ts
@@ -2,6 +2,7 @@ import auth from './auth'
import moment from 'moment'
import type { JSONSchema7 } from 'json-schema'
import { UploadedFile, FileStatus, AgentPrompt } from '../../../common/src/utils/domain'
+import type { StatusUpdate } from '../../../common/src/components/chat/ChatMessage.vue'
export class HttpError extends Error {
public status: number
@@ -29,24 +30,21 @@ export class Manifest {
id: string
contactEmail: string
auth: ManifestAuthConfig
+ disablePublishGlobal: boolean
- constructor(id: string, contactEmail: string, auth: ManifestAuthConfig) {
+ constructor(id: string, contactEmail: string, auth: ManifestAuthConfig, disablePublishGlobal: boolean) {
this.id = id
this.contactEmail = contactEmail
this.auth = auth
+ this.disablePublishGlobal = disablePublishGlobal
}
}
export const GLOBAL_TEAM_ID = 1;
-
export const MY_TEAM_ID = 0;
-
export const PRIVATE_TEAM_ID = -1;
-
export const PRIVATE_AGENT_ID = -1;
-
const PRIVATE_AGENT_ICON_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAA+NJREFUaEPtmU2IV2UUxn9PaRFoGBkVmKZ9GFFEpQRWRh+LkDYhCUVBklGR9iW5KVoMtXGIMfowWohQaKjUrlWEWS2KgsQ2ZaGDRSGlWBJl2uk+cAYGnZn/e+feYf4T/wPDfxbvfd/z3POec57zXDHFTVPcf3oAJjuCvQj8LyMQEacB04HTE+Bx4Likf9sG3OoViogzgDnAZcBiYAEQwLfAXuAbYFDSsbaAtAYgIs4F7gZWpPOzTnLyL+AL4G3gfUm/tQGiFQARcU51ZV4C7gFmA/8A+4Ef0slLgIvzWv0KbK/WPCfpcFMQbQHoB9YAZ6bTbwAfA4fSwfOApcBDwBXA38Crkp6ddAARcRuwrXLIV+h74DFg1/B7HhF+UU5qg9gIXFoB9hVaIemjJiAaRSCrzbvAcuBP4BFJW8ZyKCJWARsAJ/xbVZSeaFKdmgKYD+wArgO+Bm6XNHRtRsQREWcBnwHXAl868SX9ON4oNAWwBNgELPRblfR0iSMRMQA8lVfuYUk7S54baU1TAMuA17PCrJPkZO5oEeHkXZ+V6nFJH3R8aJQFPQC9CEDvCtXOn4hwx51XcRsn8TrgQnfW5Dkl+z2QnfvnTGYnsUmeO3Qtq53EEXF1VT0eBPxrEP4zoAPAL4WnXwBclJRi0M5XzXBPVc02S/JvsdUCEBFXVTV/K3B5dtLigwoWmmJ/V1HveyWZdhdZXQAmaOYzHlB2Z0etHfaTPHP03Mk9P/h/86hbiryHclUiIhYBHwIzgPeS9zSmw3Y06fg7wJ3AH8AdkkwzOlpxBCLC995d18+slWRW2ZpFhFnsyznBubRuLtl8PAC8b/EBJU5kFIZeUK39JwxARHg29iTmQX63pN/HAjMswpMPICJuqBL9+WSpdsilcb2kz0cD0TUAIsJJbrq8cpisciLn4GckuXmdYt0E4JpqsH+tqlg3pYxiZ90/PgVWS3L57WoAVyaAW4F96aknN5dgJ7+bVVcDsB7kgcUqxcz01LXdw06/JOtD3QsgS6L50V3Azdk3PC9vkWTOM6J1TQ4MeZcyo0Uul+ojko5OmTI6lqOTGYH7gTezNLrGDzTRc4YDSX3JKsWL2fgelWRu1NHqdGKraZ9Uytr5qeesBX7KAzseNMYCS/FzgT7gRuCgc0eSVb6OVgwgk9OK2pMp3rpEWsA1tW5i03K+MAiDeUWSo1FkdQF4dHyhGjzuA84uOqF8kbmSZcm+0br1SFvVApBRsIh7fQ42JmxtmKXFXcBXdb8b1AaQIPycxdmhT0hNQZgrHZPkrzm1bFwAap0wwYt7ACb4BXfcvheBjq9oghdM+Qj8B4wJmEDEKTttAAAAAElFTkSuQmCC';
-
const PRIVATE_AGENT_ICON_BG = '1F1F1F';
export enum LlmModelType {
@@ -90,6 +88,20 @@ export enum ReasoningEffort {
HIGH = 'HIGH'
}
+export class Evaluator {
+ modelId: string
+ temperature: LlmTemperature
+ reasoningEffort: ReasoningEffort
+ prompt: string
+
+ constructor(modelId: string, temperature: LlmTemperature, reasoningEffort: ReasoningEffort, prompt: string) {
+ this.modelId = modelId
+ this.temperature = temperature
+ this.reasoningEffort = reasoningEffort
+ this.prompt = prompt
+ }
+}
+
export enum FileProcessor {
BASIC = 'BASIC',
ENHANCED = 'ENHANCED'
@@ -171,7 +183,8 @@ export class TeamRole {
export enum Role {
TEAM_OWNER = "owner",
- TEAM_MEMBER = "member"
+ TEAM_MEMBER = "member",
+ TEAM_EDITOR = "editor"
}
export class Agent {
@@ -308,8 +321,9 @@ export class ThreadMessage {
hasPositiveFeedback?: boolean
files?: UploadedFile[]
stopped?: boolean
+ statusUpdates: StatusUpdate[] = []
- constructor(id: number, text: string, timestamp: Date, origin: ThreadMessageOrigin, children: ThreadMessage[], minutesSaved?: number, feedbackText?: string, hasPositiveFeedback?: boolean, stopped?: boolean) {
+ constructor(id: number, text: string, timestamp: Date, origin: ThreadMessageOrigin, children: ThreadMessage[], minutesSaved?: number, feedbackText?: string, hasPositiveFeedback?: boolean, stopped?: boolean, statusUpdates?: StatusUpdate[]) {
this.id = id
this.text = text
this.timestamp = timestamp
@@ -319,6 +333,7 @@ export class ThreadMessage {
this.feedbackText = feedbackText
this.hasPositiveFeedback = hasPositiveFeedback
this.stopped = stopped
+ this.statusUpdates = statusUpdates || []
}
}
@@ -455,8 +470,8 @@ export class NewUser {
role: Role;
constructor(username: string, role: Role) {
- this.username = username;
- this.role = role;
+ this.username = username;
+ this.role = role;
}
}
@@ -559,13 +574,17 @@ export class TestCaseResult {
testSuiteRunId?: number
executedAt: Date
status: TestCaseResultStatus
+ testCaseName: string
+ evaluatorAnalysis?: string
- constructor(testCaseId: number, executedAt: Date, status: TestCaseResultStatus, id?: number, testSuiteRunId?: number) {
+ constructor(testCaseId: number, executedAt: Date, status: TestCaseResultStatus, testCaseName: string, id?: number, testSuiteRunId?: number, evaluatorAnalysis?: string) {
this.testCaseId = testCaseId
this.executedAt = executedAt
this.status = status
+ this.testCaseName = testCaseName
this.testSuiteRunId = testSuiteRunId
this.id = id
+ this.evaluatorAnalysis = evaluatorAnalysis
}
}
@@ -579,7 +598,6 @@ export enum TestCaseResultStatus {
}
export type TestSuiteExecutionStreamEvent =
- | { type: 'suite.start'; data: { suiteRunId: number } }
| { type: 'suite.test.start'; data: { testCaseId: number; resultId: number } }
| { type: 'suite.test.metadata'; data: { testCaseId: number; resultId: number } }
| { type: 'suite.test.phase'; data: { phase: string; status?: string; evaluation?: any } }
@@ -589,7 +607,7 @@ export type TestSuiteExecutionStreamEvent =
| { type: 'suite.test.agentMessage.complete'; data: { id: number; text: string } }
| { type: 'suite.test.executionStatus'; data: any }
| { type: 'suite.test.error'; data: { message: string } }
- | { type: 'suite.test.complete'; data: { testCaseId: number; resultId: number; status: string } }
+ | { type: 'suite.test.complete'; data: { testCaseId: number; resultId: number; status: string; evaluation?: any } }
| { type: 'suite.complete'; data: { suiteRunId: number; status: string; totalTests: number; passed: number; failed: number; errors: number; skipped: number } }
| { type: 'suite.error'; data: {} }
@@ -779,7 +797,7 @@ export class ApiService {
return await this.fetchJson(`/agents/${agentId}/tools/${toolId}/files`)
}
- async findAgentDocToolFile(agentId: number, toolId: string, fileId: number): Promise