Skip to content

Commit 99d7ca5

Browse files
abossardCopilot
andcommitted
fix: LiteLLM fallback in agent_builder + add live lifecycle test
- Fixed agent_builder/engine/react_runner.py: ChatLiteLLM when no API key - Fixed agent_builder/service.py: removed hard OpenAI key requirement - Fixed agent_builder/chat_service.py: same - Fixed RunsSidePanel output parsing for raw string output - Added full lifecycle e2e test (live LLM): create → run → edit → re-run → verify history → delete Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 616fe88 commit 99d7ca5

5 files changed

Lines changed: 140 additions & 21 deletions

File tree

backend/agent_builder/chat_service.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ def __init__(
6060
@property
6161
def llm(self) -> Any:
6262
if self._llm is None:
63-
if not self._api_key:
64-
raise ValueError("OPENAI_API_KEY is required.")
6563
self._llm = build_llm(self._model, self._api_key, self._base_url)
6664
return self._llm
6765

backend/agent_builder/engine/react_runner.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010

1111
import logging
12+
import os
1213
from dataclasses import dataclass, field
1314
from typing import Any
1415

@@ -34,20 +35,24 @@ def build_llm(
3435
max_tokens: int = 0,
3536
reasoning_effort: str = "low",
3637
) -> Any:
37-
"""Construct a ChatOpenAI instance with configurable reasoning effort."""
38-
from langchain_openai import ChatOpenAI
39-
40-
kwargs: dict[str, Any] = {
41-
"model": model,
42-
"api_key": api_key,
43-
"base_url": base_url or None,
44-
"temperature": temperature,
45-
}
46-
if max_tokens > 0:
47-
kwargs["max_tokens"] = max_tokens
48-
if reasoning_effort and reasoning_effort != "default":
49-
kwargs["reasoning_effort"] = reasoning_effort
50-
return ChatOpenAI(**kwargs)
38+
"""Construct an LLM instance — ChatOpenAI when api_key is provided, ChatLiteLLM otherwise."""
39+
if api_key:
40+
from langchain_openai import ChatOpenAI
41+
kwargs: dict[str, Any] = {
42+
"model": model,
43+
"api_key": api_key,
44+
"base_url": base_url or None,
45+
"temperature": temperature,
46+
}
47+
if max_tokens > 0:
48+
kwargs["max_tokens"] = max_tokens
49+
if reasoning_effort and reasoning_effort != "default":
50+
kwargs["reasoning_effort"] = reasoning_effort
51+
return ChatOpenAI(**kwargs)
52+
else:
53+
from langchain_litellm import ChatLiteLLM
54+
litellm_model = os.getenv("LITELLM_MODEL", "github_copilot/gpt-4o")
55+
return ChatLiteLLM(model=litellm_model, temperature=temperature)
5156

5257

5358
def build_react_agent(

backend/agent_builder/service.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ def __init__(
7272
@property
7373
def llm(self) -> Any:
7474
if self._llm is None:
75-
if not self._api_key:
76-
raise ValueError(
77-
"OPENAI_API_KEY is required to run agents. "
78-
"Set it via environment variable or pass openai_api_key."
79-
)
8075
self._llm = build_llm(self._model, self._api_key, self._base_url, reasoning_effort="low")
8176
return self._llm
8277

backend/agent_workbench/service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def on_tool_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) ->
7777
def _build_llm(model: str, api_key: str, base_url: str = "") -> Any:
7878
if api_key:
7979
from langchain_openai import ChatOpenAI
80+
logger.info("Workbench LLM: using ChatOpenAI (model=%s)", model)
8081
return ChatOpenAI(
8182
model=model,
8283
api_key=api_key,
@@ -86,6 +87,7 @@ def _build_llm(model: str, api_key: str, base_url: str = "") -> Any:
8687
else:
8788
from langchain_litellm import ChatLiteLLM
8889
litellm_model = os.getenv("LITELLM_MODEL", "github_copilot/gpt-4o")
90+
logger.info("Workbench LLM: using ChatLiteLLM (model=%s)", litellm_model)
8991
return ChatLiteLLM(
9092
model=litellm_model,
9193
temperature=0.0,

tests/e2e/workbench.spec.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,125 @@ test.describe("Agent Fabric UI", () => {
380380
});
381381
});
382382

383+
// ---------------------------------------------------------------------------
384+
// Full Agent Lifecycle — Live LLM integration test
385+
// ---------------------------------------------------------------------------
386+
387+
test.describe("Agent Lifecycle (live)", () => {
388+
test("creates, runs, edits, re-runs, checks history, and deletes an agent", async ({ page }) => {
389+
test.setTimeout(120_000); // LLM calls can be slow
390+
391+
const agentName = `e2e-lifecycle-${Date.now()}`;
392+
const initialPrompt =
393+
"Suche VPN-bezogene Tickets mit csv_search_tickets. Liste die gefundenen Ticket-IDs und eine kurze Zusammenfassung. Antworte auf Deutsch.";
394+
const editedPrompt =
395+
"Nutze csv_search_tickets um Tickets zum Thema 'Outlook' zu finden. Gib die Anzahl und die Ticket-IDs zurück. Antworte auf Deutsch.";
396+
397+
// Clean up any leftover e2e agents from previous runs
398+
const existingAgents = await page.request.get(`${BACKEND_URL}/api/workbench/agents`);
399+
const agentsList = (await existingAgents.json()).agents || [];
400+
for (const a of agentsList) {
401+
if (a.name.includes("e2e-lifecycle")) {
402+
await page.request.delete(`${BACKEND_URL}/api/workbench/agents/${a.id}`);
403+
}
404+
}
405+
406+
// --- 1. Create agent ---
407+
await createAgent(page, {
408+
name: agentName,
409+
description: "E2E lifecycle test — VPN search agent",
410+
systemPrompt: initialPrompt,
411+
});
412+
413+
// Verify agent card is visible
414+
const card = page.locator(`[data-testid^="agent-card-"]`, { hasText: agentName });
415+
await expect(card).toBeVisible({ timeout: 10000 });
416+
417+
// --- 2. Run the agent (first run — VPN search) ---
418+
const runBtn = card.locator('button', { hasText: "Run" });
419+
await runBtn.click();
420+
421+
// Wait for the run to appear in the Runs side panel
422+
const runsPanel = page.getByTestId("runs-side-panel");
423+
const runEntries = runsPanel.locator('[data-testid^="run-entry-"]');
424+
const initialRunCount = await runEntries.count();
425+
const firstRunEntry = runEntries.first();
426+
await expect(firstRunEntry).toBeVisible({ timeout: 60000 });
427+
await expect(firstRunEntry).toContainText("completed", { timeout: 60000 });
428+
429+
// Click the run entry to see its detail
430+
await firstRunEntry.click();
431+
const firstRunDetail = runsPanel.locator('[data-testid^="run-detail-"]').first();
432+
await expect(firstRunDetail).toBeVisible({ timeout: 5000 });
433+
// First run should mention VPN-related content
434+
await expect(firstRunDetail).toContainText(/VPN|vpn|Ticket/i, { timeout: 5000 });
435+
436+
// --- 3. Edit the agent — change prompt + add requires_input ---
437+
const editBtn = card.locator('[data-testid^="agent-card-edit-"]');
438+
await editBtn.click();
439+
440+
// Wait for edit dialog
441+
const dialog = page.getByTestId("agent-edit-dialog");
442+
await expect(dialog).toBeVisible({ timeout: 5000 });
443+
444+
// Change system prompt
445+
const promptField = dialog.getByTestId("edit-agent-system-prompt");
446+
await promptField.clear();
447+
await promptField.fill(editedPrompt);
448+
449+
// Enable requires_input
450+
const requiresInputCheckbox = dialog.getByTestId("edit-agent-requires-input");
451+
await requiresInputCheckbox.click();
452+
const inputDescField = dialog.getByTestId("edit-agent-required-input-desc");
453+
await expect(inputDescField).toBeVisible();
454+
await inputDescField.fill("Suchbegriff");
455+
456+
// Save
457+
await dialog.getByTestId("edit-agent-save").click();
458+
await expect(dialog).not.toBeVisible({ timeout: 10000 });
459+
460+
// --- 4. Run the edited agent (second run — Outlook search with input) ---
461+
// Card should now show an input field when clicking Run
462+
await page.waitForTimeout(500); // Let card refresh
463+
const runBtn2 = card.locator('button', { hasText: "Run" });
464+
await runBtn2.click();
465+
466+
// Should show input field (requires_input is now true)
467+
const inputField = card.locator('input[placeholder]');
468+
await expect(inputField).toBeVisible({ timeout: 5000 });
469+
await inputField.fill("Outlook");
470+
await card.locator('button', { hasText: "Go" }).click();
471+
472+
// Wait for second run to appear (at least one more than after first run)
473+
await expect(runEntries).toHaveCount(initialRunCount + 2, { timeout: 60000 });
474+
475+
// The newest run (first in list) should complete
476+
const secondRunEntry = runEntries.first();
477+
await expect(secondRunEntry).toContainText("completed", { timeout: 60000 });
478+
479+
// --- 5. Verify run history — both runs visible with different output ---
480+
// Click first run (newest = Outlook)
481+
await secondRunEntry.click();
482+
const secondRunDetail = runsPanel.locator('[data-testid^="run-detail-"]').first();
483+
await expect(secondRunDetail).toBeVisible();
484+
await expect(secondRunDetail).toContainText(/Outlook|outlook|Ticket/i, { timeout: 5000 });
485+
486+
// Click second run (older = VPN)
487+
const olderRunEntry = runEntries.nth(1);
488+
await olderRunEntry.click();
489+
await page.waitForTimeout(300);
490+
const olderRunDetail = runsPanel.locator('[data-testid^="run-detail-"]').first();
491+
await expect(olderRunDetail).toContainText(/VPN|vpn|Ticket/i, { timeout: 5000 });
492+
493+
// --- 6. Delete the agent ---
494+
const deleteBtn = card.locator('[data-testid^="agent-card-delete-"]');
495+
await deleteBtn.click();
496+
497+
// Agent card should disappear
498+
await expect(card).not.toBeVisible({ timeout: 10000 });
499+
});
500+
});
501+
383502
// ---------------------------------------------------------------------------
384503
// Agent Chat UI (unchanged — tests /agent page, not workbench)
385504
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)