Skip to content

Commit bec2b17

Browse files
committed
feat: Add required input handling to agent definitions and update UI components
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent f90a98a commit bec2b17

6 files changed

Lines changed: 330 additions & 20 deletions

File tree

backend/agent_workbench/models.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ class AgentDefinition(SQLModel, table=True):
6868
name: str = SField(index=True, description="Human-readable agent name")
6969
description: str = SField(default="", description="Optional description")
7070
system_prompt: str = SField(description="System prompt sent to the LLM")
71+
requires_input: bool = SField(
72+
default=False,
73+
description="When true, runs must include required_input_value",
74+
)
75+
required_input_description: str = SField(
76+
default="",
77+
description="Description shown to operators for required runtime input",
78+
)
7179
# Stored as JSON arrays in TEXT columns
7280
tool_names_json: str = SField(
7381
default="[]",
@@ -115,6 +123,8 @@ def to_dict(self) -> dict[str, Any]:
115123
"name": self.name,
116124
"description": self.description,
117125
"system_prompt": self.system_prompt,
126+
"requires_input": self.requires_input,
127+
"required_input_description": self.required_input_description,
118128
"tool_names": self.tool_names,
119129
"success_criteria": [c.model_dump() for c in self.success_criteria],
120130
"created_at": self.created_at.isoformat(),
@@ -228,6 +238,8 @@ class AgentDefinitionCreate(BaseModel):
228238
name: str = Field(..., min_length=1, max_length=200)
229239
description: str = Field(default="")
230240
system_prompt: str = Field(..., min_length=1)
241+
requires_input: bool = Field(default=False)
242+
required_input_description: str = Field(default="")
231243
tool_names: list[str] = Field(default_factory=list)
232244
success_criteria: list[SuccessCriteria] = Field(default_factory=list)
233245

@@ -236,12 +248,15 @@ class AgentDefinitionUpdate(BaseModel):
236248
name: Optional[str] = Field(default=None)
237249
description: Optional[str] = Field(default=None)
238250
system_prompt: Optional[str] = Field(default=None)
251+
requires_input: Optional[bool] = Field(default=None)
252+
required_input_description: Optional[str] = Field(default=None)
239253
tool_names: Optional[list[str]] = Field(default=None)
240254
success_criteria: Optional[list[SuccessCriteria]] = Field(default=None)
241255

242256

243257
class AgentRunCreate(BaseModel):
244-
input_prompt: str = Field(..., min_length=1, max_length=10000)
258+
input_prompt: str = Field(default="", max_length=10000)
259+
required_input_value: Optional[str] = Field(default=None, max_length=2000)
245260

246261

247262
# ============================================================================
@@ -258,6 +273,18 @@ def build_engine(db_path: Path):
258273

259274
def _run_migrations(engine) -> None:
260275
"""Apply lightweight SQLite migrations for new columns."""
276+
_ensure_column(
277+
engine,
278+
"workbench_agent_definitions",
279+
"requires_input",
280+
"BOOLEAN NOT NULL DEFAULT 0",
281+
)
282+
_ensure_column(
283+
engine,
284+
"workbench_agent_definitions",
285+
"required_input_description",
286+
"TEXT NOT NULL DEFAULT ''",
287+
)
261288
_ensure_column(
262289
engine,
263290
"workbench_agent_runs",

backend/agent_workbench/service.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,56 @@ def _build_agent_snapshot(self, agent: AgentDefinition) -> dict[str, Any]:
158158
"name": agent.name,
159159
"description": agent.description,
160160
"system_prompt": agent.system_prompt,
161+
"requires_input": agent.requires_input,
162+
"required_input_description": agent.required_input_description,
161163
"tool_names": list(agent.tool_names),
162164
"success_criteria": [criteria.model_dump() for criteria in agent.success_criteria],
163165
"captured_at": datetime.now().isoformat(),
164166
}
165167

168+
def _normalize_input_contract(
169+
self,
170+
requires_input: bool,
171+
required_input_description: str,
172+
) -> tuple[bool, str]:
173+
normalized_description = (required_input_description or "").strip()
174+
if requires_input and not normalized_description:
175+
raise ValueError(
176+
"required_input_description must be provided when requires_input is true"
177+
)
178+
if not requires_input:
179+
normalized_description = ""
180+
return requires_input, normalized_description
181+
182+
def _build_run_user_message(
183+
self,
184+
agent_def: AgentDefinition,
185+
run_request: AgentRunCreate,
186+
) -> tuple[str, str]:
187+
run_prompt = (run_request.input_prompt or "").strip()
188+
required_input_value = (run_request.required_input_value or "").strip()
189+
message_parts: list[str] = []
190+
191+
if run_prompt:
192+
message_parts.append(run_prompt)
193+
194+
if agent_def.requires_input:
195+
if not required_input_value:
196+
raise ValueError(
197+
"Missing required_input_value for this agent. "
198+
f"Expected: {agent_def.required_input_description}"
199+
)
200+
message_parts.append(
201+
f"Required input ({agent_def.required_input_description}): {required_input_value}"
202+
)
203+
elif required_input_value:
204+
message_parts.append(f"Additional input: {required_input_value}")
205+
206+
if not message_parts:
207+
message_parts.append("Proceed with the configured system instructions and tools.")
208+
209+
return "\n\n".join(message_parts), required_input_value
210+
166211
def _criteria_from_run_snapshot(self, run: AgentRun) -> list[SuccessCriteria]:
167212
snapshot = run.agent_snapshot
168213
raw = snapshot.get("success_criteria")
@@ -184,10 +229,16 @@ def _criteria_from_run_snapshot(self, run: AgentRun) -> list[SuccessCriteria]:
184229

185230
def create_agent(self, data: AgentDefinitionCreate) -> AgentDefinition:
186231
validated_tool_names = self._validate_tool_names(data.tool_names)
232+
requires_input, required_input_description = self._normalize_input_contract(
233+
data.requires_input,
234+
data.required_input_description,
235+
)
187236
agent = AgentDefinition(
188237
name=data.name,
189238
description=data.description,
190239
system_prompt=data.system_prompt,
240+
requires_input=requires_input,
241+
required_input_description=required_input_description,
191242
)
192243
agent.tool_names = validated_tool_names
193244
agent.success_criteria = data.success_criteria
@@ -218,6 +269,19 @@ def update_agent(
218269
agent.description = data.description
219270
if data.system_prompt is not None:
220271
agent.system_prompt = data.system_prompt
272+
next_requires_input = agent.requires_input if data.requires_input is None else data.requires_input
273+
next_required_input_description = (
274+
agent.required_input_description
275+
if data.required_input_description is None
276+
else data.required_input_description
277+
)
278+
(
279+
agent.requires_input,
280+
agent.required_input_description,
281+
) = self._normalize_input_contract(
282+
next_requires_input,
283+
next_required_input_description,
284+
)
221285
if data.tool_names is not None:
222286
agent.tool_names = self._validate_tool_names(data.tool_names)
223287
if data.success_criteria is not None:
@@ -279,11 +343,16 @@ async def run_agent(
279343

280344
validated_tool_names = self._validate_tool_names(agent_def.tool_names)
281345
agent_snapshot = self._build_agent_snapshot(agent_def)
346+
user_message, normalized_required_input = self._build_run_user_message(agent_def, run_request)
347+
normalized_prompt = (run_request.input_prompt or "").strip()
348+
agent_snapshot["input_prompt"] = normalized_prompt
349+
agent_snapshot["required_input_value"] = normalized_required_input
350+
agent_snapshot["composed_user_message"] = user_message
282351

283352
# -- Persist a PENDING run --
284353
run = AgentRun(
285354
agent_id=agent_id,
286-
input_prompt=run_request.input_prompt,
355+
input_prompt=normalized_prompt,
287356
status=RunStatus.RUNNING.value,
288357
)
289358
run.agent_snapshot = agent_snapshot
@@ -300,7 +369,7 @@ async def run_agent(
300369
react = _build_react_agent(self.llm, tools, agent_def.system_prompt)
301370

302371
result = await react.ainvoke(
303-
{"messages": [("user", run_request.input_prompt)]},
372+
{"messages": [("user", user_message)]},
304373
config={"recursion_limit": self._recursion_limit},
305374
)
306375

backend/tests/test_workbench_integration_e2e.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ def __init__(self, tools: list[object]) -> None:
3030

3131
async def ainvoke(self, _payload: dict, config: dict | None = None) -> dict:
3232
_ = config
33+
user_message = ""
34+
messages = _payload.get("messages", [])
35+
if messages:
36+
first = messages[0]
37+
if isinstance(first, (list, tuple)) and len(first) >= 2:
38+
user_message = str(first[1])
39+
3340
tool = next(
3441
(item for item in self._tools if getattr(item, "name", "") == "csv_ticket_stats"),
3542
None,
@@ -39,7 +46,7 @@ async def ainvoke(self, _payload: dict, config: dict | None = None) -> dict:
3946

4047
stats = await tool.ainvoke({})
4148
total = stats.get("total", 0) if isinstance(stats, dict) else 0
42-
output = f"Used csv_ticket_stats successfully. total={total}"
49+
output = f"Used csv_ticket_stats successfully. total={total}. context={user_message}"
4350
return {
4451
"messages": [
4552
_ToolCallMessage("csv_ticket_stats"),
@@ -141,6 +148,67 @@ async def test_create_run_and_evaluate_agent_with_csv_tool(self) -> None:
141148
self.assertTrue(evaluate_data["overall_passed"])
142149
self.assertEqual(evaluate_data["score"], 1.0)
143150

151+
async def test_required_input_agent_run_validation_and_context(self) -> None:
152+
with patch("agent_workbench.service._build_react_agent", new=_fake_build_react_agent):
153+
async with backend_app_module.app.test_app() as test_app:
154+
client = test_app.test_client()
155+
156+
invalid_create_resp = await client.post(
157+
"/api/workbench/agents",
158+
json={
159+
"name": "Needs Input Invalid",
160+
"description": "",
161+
"system_prompt": "Use CSV tools.",
162+
"requires_input": True,
163+
"required_input_description": "",
164+
"tool_names": ["csv_ticket_stats"],
165+
"success_criteria": [],
166+
},
167+
)
168+
self.assertEqual(invalid_create_resp.status_code, 400)
169+
170+
create_resp = await client.post(
171+
"/api/workbench/agents",
172+
json={
173+
"name": "Needs Input",
174+
"description": "",
175+
"system_prompt": "Use CSV tools.",
176+
"requires_input": True,
177+
"required_input_description": "Ticket INC number",
178+
"tool_names": ["csv_ticket_stats"],
179+
"success_criteria": [],
180+
},
181+
)
182+
create_data = await create_resp.get_json()
183+
self.assertEqual(create_resp.status_code, 201, create_data)
184+
self.assertTrue(create_data["requires_input"])
185+
self.assertEqual(create_data["required_input_description"], "Ticket INC number")
186+
agent_id = create_data["id"]
187+
188+
missing_input_resp = await client.post(
189+
f"/api/workbench/agents/{agent_id}/runs",
190+
json={"input_prompt": ""},
191+
)
192+
self.assertEqual(missing_input_resp.status_code, 400)
193+
194+
run_resp = await client.post(
195+
f"/api/workbench/agents/{agent_id}/runs",
196+
json={"required_input_value": "INC-12345"},
197+
)
198+
run_data = await run_resp.get_json()
199+
self.assertEqual(run_resp.status_code, 200, run_data)
200+
self.assertEqual(run_data["status"], "completed")
201+
self.assertEqual(run_data["input_prompt"], "")
202+
self.assertIn("INC-12345", run_data["output"] or "")
203+
self.assertEqual(
204+
run_data["agent_snapshot"].get("required_input_value"),
205+
"INC-12345",
206+
)
207+
self.assertIn(
208+
"Required input (Ticket INC number): INC-12345",
209+
run_data["agent_snapshot"].get("composed_user_message", ""),
210+
)
211+
144212

145213
if __name__ == "__main__":
146214
unittest.main()

0 commit comments

Comments
 (0)