Skip to content

Commit 175268f

Browse files
committed
feat: Implement Tool Registry and Workbench Integration
- Added ToolRegistry class to manage LangChain StructuredTool instances. - Created workbench_integration.py to wire tools into the Agent Workbench. - Developed WorkbenchPage component for agent management in the frontend. - Implemented backend tests for tool registration and agent operations. - Added end-to-end tests for agent creation and deletion in the UI. Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent d1a653a commit 175268f

23 files changed

Lines changed: 2003 additions & 349 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ Thumbs.db
5151
logs/
5252
*.db
5353
csv/*.csv
54+
screenshots/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Agent Workbench - Public API
3+
4+
Import everything you need from this package:
5+
6+
from agent_workbench import (
7+
WorkbenchService,
8+
ToolRegistry,
9+
AgentDefinitionCreate,
10+
AgentRunCreate,
11+
SuccessCriteria,
12+
CriteriaType,
13+
)
14+
"""
15+
16+
from .evaluator import compute_score, evaluate_run
17+
from .models import (
18+
AgentDefinition,
19+
AgentDefinitionCreate,
20+
AgentDefinitionUpdate,
21+
AgentEvaluation,
22+
AgentRun,
23+
AgentRunCreate,
24+
CriteriaResult,
25+
CriteriaType,
26+
RunStatus,
27+
SuccessCriteria,
28+
)
29+
from .service import WorkbenchService
30+
from .tool_registry import ToolRegistry
31+
32+
__all__ = [
33+
# Service
34+
"WorkbenchService",
35+
"ToolRegistry",
36+
# Models
37+
"AgentDefinition",
38+
"AgentDefinitionCreate",
39+
"AgentDefinitionUpdate",
40+
"AgentEvaluation",
41+
"AgentRun",
42+
"AgentRunCreate",
43+
"CriteriaResult",
44+
"CriteriaType",
45+
"RunStatus",
46+
"SuccessCriteria",
47+
# Evaluator helpers (useful for tests)
48+
"compute_score",
49+
"evaluate_run",
50+
]
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Agent Workbench - Evaluator
3+
4+
Applies SuccessCriteria to a completed AgentRun and produces CriteriaResult list.
5+
6+
Supported criteria types:
7+
no_error - run completed without an error
8+
tool_called - a specific tool name appears in tools_used
9+
output_contains - final output contains the substring (case-insensitive)
10+
llm_judge - the LLM grades the output via a judge prompt (requires LLM configuration)
11+
"""
12+
13+
from typing import Any
14+
15+
from .models import AgentRun, CriteriaResult, CriteriaType, SuccessCriteria
16+
17+
# ============================================================================
18+
# ASYNC LLM JUDGE (I/O)
19+
# ============================================================================
20+
21+
async def _eval_llm_judge(
22+
run: AgentRun,
23+
criteria: SuccessCriteria,
24+
llm: Any, # ChatOpenAI or compatible
25+
) -> CriteriaResult:
26+
"""
27+
Ask an LLM to evaluate whether the run output satisfies the judge prompt.
28+
29+
The criteria.value is a judge prompt that is appended with the run's
30+
output. The LLM must respond with PASS or FAIL (case-insensitive) as
31+
the first word.
32+
"""
33+
if llm is None:
34+
raise ValueError("llm_judge criteria require an LLM instance")
35+
36+
judge_prompt = (
37+
f"{criteria.value}\n\n"
38+
f"--- Agent Output ---\n{run.output or '(empty)'}\n\n"
39+
"Reply with a single word: PASS or FAIL, followed by an optional explanation."
40+
)
41+
42+
try:
43+
from langchain_core.messages import HumanMessage
44+
response = await llm.ainvoke([HumanMessage(content=judge_prompt)])
45+
answer = (response.content or "").strip()
46+
passed = answer.upper().startswith("PASS")
47+
return CriteriaResult(
48+
criteria=criteria,
49+
passed=passed,
50+
detail=answer[:500],
51+
)
52+
except Exception as exc:
53+
return CriteriaResult(
54+
criteria=criteria,
55+
passed=False,
56+
detail=f"LLM judge error: {exc}",
57+
)
58+
59+
60+
# ============================================================================
61+
# PUBLIC EVALUATOR
62+
# ============================================================================
63+
64+
async def evaluate_run(
65+
run: AgentRun,
66+
criteria_list: list[SuccessCriteria],
67+
llm: Any = None,
68+
) -> list[CriteriaResult]:
69+
"""
70+
Evaluate all criteria for a run.
71+
72+
Args:
73+
run: Completed AgentRun (status = completed | failed).
74+
criteria_list: List of SuccessCriteria from the AgentDefinition.
75+
llm: Optional LLM instance; required when criteria include llm_judge.
76+
77+
Returns:
78+
Ordered list of CriteriaResult, one per criterion.
79+
"""
80+
results: list[CriteriaResult] = []
81+
82+
for criteria in criteria_list:
83+
if criteria.type == CriteriaType.NO_ERROR:
84+
passed = run.error is None and run.status == "completed"
85+
results.append(
86+
CriteriaResult(
87+
criteria=criteria,
88+
passed=passed,
89+
detail="" if passed else f"Run error: {run.error or 'unexpected status ' + run.status}",
90+
)
91+
)
92+
elif criteria.type == CriteriaType.TOOL_CALLED:
93+
tool_name = criteria.value.strip()
94+
results.append(
95+
CriteriaResult(
96+
criteria=criteria,
97+
passed=tool_name in run.tools_used,
98+
detail=f"tools_used={run.tools_used}",
99+
)
100+
)
101+
elif criteria.type == CriteriaType.OUTPUT_CONTAINS:
102+
needle = criteria.value
103+
haystack = (run.output or "").lower()
104+
results.append(
105+
CriteriaResult(
106+
criteria=criteria,
107+
passed=needle.lower() in haystack,
108+
detail=f"searched for '{needle}' in output ({len(run.output or '')} chars)",
109+
)
110+
)
111+
elif criteria.type == CriteriaType.LLM_JUDGE:
112+
results.append(await _eval_llm_judge(run, criteria, llm))
113+
else:
114+
results.append(CriteriaResult(
115+
criteria=criteria,
116+
passed=False,
117+
detail=f"Unknown criteria type: {criteria.type}",
118+
))
119+
120+
return results
121+
122+
123+
def compute_score(results: list[CriteriaResult]) -> float:
124+
"""Returns the fraction of passed criteria (0.0 when no criteria)."""
125+
if not results:
126+
return 1.0 # vacuously true
127+
passed = sum(1 for r in results if r.passed)
128+
return round(passed / len(results), 4)

0 commit comments

Comments
 (0)