Skip to content

Commit 8fa5762

Browse files
majdyzclaude
andcommitted
feat(platform): enhance Human-in-the-Loop block with improved review UX
- Backend: Fix message handling and unify API structure - Fix HITL block to properly yield review messages for both approved and rejected reviews - Unify review API structure with single `reviews` array using `approved` boolean field - Remove separate approved_reviews/rejected_review_ids in favor of cleaner unified approach - Frontend: Complete UI/UX overhaul for review interface - Replace plain JSON textarea with type-aware input components matching run dialog styling - Add "Approve All" and "Reject All" buttons with smart disabled states - Show rejection reason input only when excluding items (simplified UX) - Fix Reviews tab auto-population when execution status changes to REVIEW - Add proper local state management for real-time input updates - Use design system Input components for consistent rounded styling Key improvements: - No more JSON syntax errors for string inputs - Professional appearance matching platform standards - Intuitive workflow with conditional UI elements - Type-safe unified API structure - Real-time input updates with proper state management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 39ec38f commit 8fa5762

File tree

8 files changed

+266
-140
lines changed

8 files changed

+266
-140
lines changed

autogpt_platform/backend/backend/blocks/human_in_the_loop.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,13 @@ async def run(
148148
node_exec_id=node_exec_id, processed=True
149149
)
150150

151-
# Yield the results
152151
if result.status == ReviewStatus.APPROVED:
153-
yield "reviewed_data", result.data
154152
yield "status", "approved"
155-
yield "review_message", result.message
153+
yield "reviewed_data", result.data
154+
if result.message:
155+
yield "review_message", result.message
156+
156157
elif result.status == ReviewStatus.REJECTED:
157158
yield "status", "rejected"
158-
yield "review_message", result.message
159+
if result.message:
160+
yield "review_message", result.message

autogpt_platform/backend/backend/server/v2/executions/review/model.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,15 @@ class ReviewItem(BaseModel):
9898
"""Single review item for processing."""
9999

100100
node_exec_id: str = Field(description="Node execution ID to review")
101+
approved: bool = Field(
102+
description="Whether this review is approved (True) or rejected (False)"
103+
)
101104
message: str | None = Field(
102105
None, description="Optional review message", max_length=2000
103106
)
104-
reviewed_data: SafeJsonData | None = Field(None, description="Optional edited data")
107+
reviewed_data: SafeJsonData | None = Field(
108+
None, description="Optional edited data (ignored if approved=False)"
109+
)
105110

106111
@field_validator("reviewed_data")
107112
@classmethod
@@ -168,31 +173,24 @@ class ReviewRequest(BaseModel):
168173
169174
This request must include ALL pending reviews for a graph execution.
170175
Each review will be either approved (with optional data modifications)
171-
or rejected. The execution will resume only after ALL reviews are processed.
176+
or rejected (data ignored). The execution will resume only after ALL reviews are processed.
172177
"""
173178

174-
approved_reviews: List[ReviewItem] = Field(
175-
default=[], description="Reviews to approve with their data and messages"
176-
)
177-
rejected_review_ids: List[str] = Field(
178-
default=[], description="Node execution IDs of reviews to reject"
179+
reviews: List[ReviewItem] = Field(
180+
description="All reviews with their approval status, data, and messages"
179181
)
180182

181183
@model_validator(mode="after")
182184
def validate_review_completeness(self):
183-
"""Validate that we have at least one review to process and no overlaps."""
184-
if not self.approved_reviews and not self.rejected_review_ids:
185+
"""Validate that we have at least one review to process and no duplicates."""
186+
if not self.reviews:
185187
raise ValueError("At least one review must be provided")
186188

187-
# Ensure no duplicate node_exec_ids between approved and rejected
188-
approved_ids = {review.node_exec_id for review in self.approved_reviews}
189-
rejected_ids = set(self.rejected_review_ids)
190-
191-
overlap = approved_ids & rejected_ids
192-
if overlap:
193-
raise ValueError(
194-
f"Review IDs cannot be both approved and rejected: {', '.join(overlap)}"
195-
)
189+
# Ensure no duplicate node_exec_ids
190+
node_ids = [review.node_exec_id for review in self.reviews]
191+
if len(node_ids) != len(set(node_ids)):
192+
duplicates = [nid for nid in set(node_ids) if node_ids.count(nid) > 1]
193+
raise ValueError(f"Duplicate review IDs found: {', '.join(duplicates)}")
196194

197195
return self
198196

autogpt_platform/backend/backend/server/v2/executions/review/routes.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,7 @@ async def process_review_action(
139139
"""Process reviews with approve or reject actions."""
140140

141141
# Collect all node exec IDs from the request
142-
all_request_node_ids = set()
143-
if request.approved_reviews:
144-
all_request_node_ids.update(
145-
review.node_exec_id for review in request.approved_reviews
146-
)
147-
if request.rejected_review_ids:
148-
all_request_node_ids.update(request.rejected_review_ids)
142+
all_request_node_ids = {review.node_exec_id for review in request.reviews}
149143

150144
if not all_request_node_ids:
151145
raise HTTPException(
@@ -156,18 +150,19 @@ async def process_review_action(
156150
try:
157151
# Build review decisions map
158152
review_decisions = {}
159-
for review in request.approved_reviews:
160-
review_decisions[review.node_exec_id] = (
161-
ReviewStatus.APPROVED,
162-
review.reviewed_data,
163-
review.message,
164-
)
165-
for node_id in request.rejected_review_ids:
166-
review_decisions[node_id] = (
167-
ReviewStatus.REJECTED,
168-
None,
169-
"Rejected by user",
170-
)
153+
for review in request.reviews:
154+
if review.approved:
155+
review_decisions[review.node_exec_id] = (
156+
ReviewStatus.APPROVED,
157+
review.reviewed_data,
158+
review.message,
159+
)
160+
else:
161+
review_decisions[review.node_exec_id] = (
162+
ReviewStatus.REJECTED,
163+
None,
164+
review.message,
165+
)
171166

172167
# Process all reviews
173168
updated_reviews = await process_all_reviews_for_execution(

autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/SelectedRunView/SelectedRunView.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useEffect } from "react";
44
import {
55
TabsLine,
66
TabsLineContent,
@@ -50,6 +50,12 @@ export function SelectedRunView({
5050
parseAsString.withDefault("output"),
5151
);
5252

53+
useEffect(() => {
54+
if (run?.status === AgentExecutionStatus.REVIEW && runId) {
55+
refetchReviews();
56+
}
57+
}, [run?.status, runId, refetchReviews]);
58+
5359
if (responseError || httpError) {
5460
return (
5561
<ErrorCard
@@ -114,22 +120,25 @@ export function SelectedRunView({
114120
</RunDetailCard>
115121
</TabsLineContent>
116122

117-
{pendingReviews.length > 0 &&
118-
run?.status === AgentExecutionStatus.REVIEW && (
119-
<TabsLineContent value="reviews">
120-
<RunDetailCard>
121-
{reviewsLoading ? (
122-
<div className="text-neutral-500">Loading reviews…</div>
123-
) : (
124-
<PendingReviewsList
125-
reviews={pendingReviews}
126-
onReviewComplete={refetchReviews}
127-
emptyMessage="No pending reviews for this execution"
128-
/>
129-
)}
130-
</RunDetailCard>
131-
</TabsLineContent>
132-
)}
123+
{run?.status === AgentExecutionStatus.REVIEW && (
124+
<TabsLineContent value="reviews">
125+
<RunDetailCard>
126+
{reviewsLoading ? (
127+
<div className="text-neutral-500">Loading reviews…</div>
128+
) : pendingReviews.length > 0 ? (
129+
<PendingReviewsList
130+
reviews={pendingReviews}
131+
onReviewComplete={refetchReviews}
132+
emptyMessage="No pending reviews for this execution"
133+
/>
134+
) : (
135+
<div className="text-neutral-600">
136+
No pending reviews for this execution
137+
</div>
138+
)}
139+
</RunDetailCard>
140+
</TabsLineContent>
141+
)}
133142
</TabsLine>
134143
</div>
135144
);

autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
import moment from "moment";
3-
import React, { useCallback, useMemo } from "react";
3+
import React, { useCallback, useMemo, useEffect } from "react";
44

55
import {
66
Graph,
@@ -77,6 +77,13 @@ export function AgentRunDetailsView({
7777

7878
const toastOnFail = useToastOnFail();
7979

80+
// Refetch pending reviews when execution status changes to REVIEW
81+
useEffect(() => {
82+
if (runStatus === "review" && run.id) {
83+
refetchReviews();
84+
}
85+
}, [runStatus, run.id, refetchReviews]);
86+
8087
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
8188
if (!run) return [];
8289
return [
@@ -382,7 +389,7 @@ export function AgentRunDetailsView({
382389
)}
383390

384391
{/* Pending Reviews Section */}
385-
{pendingReviews.length > 0 && (
392+
{runStatus === "review" && (
386393
<Card className="agpt-box">
387394
<CardHeader>
388395
<CardTitle className="font-poppins text-lg">
@@ -392,12 +399,16 @@ export function AgentRunDetailsView({
392399
<CardContent>
393400
{reviewsLoading ? (
394401
<LoadingBox spinnerSize={12} className="h-24" />
395-
) : (
402+
) : pendingReviews.length > 0 ? (
396403
<PendingReviewsList
397404
reviews={pendingReviews}
398405
onReviewComplete={refetchReviews}
399406
emptyMessage="No pending reviews for this execution"
400407
/>
408+
) : (
409+
<div className="py-4 text-neutral-600">
410+
No pending reviews for this execution
411+
</div>
401412
)}
402413
</CardContent>
403414
</Card>

autogpt_platform/frontend/src/app/api/openapi.json

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2988,7 +2988,7 @@
29882988
"delete": {
29892989
"tags": ["v2", "store", "private"],
29902990
"summary": "Delete store submission",
2991-
"description": "Delete a store listing submission.\n\nArgs:\n user_id (str): ID of the authenticated user\n submission_id (str): ID of the submission to be deleted\n\nReturns:\n StoreSubmission: The deleted submission object",
2991+
"description": "Delete a store listing submission.\n\nArgs:\n user_id (str): ID of the authenticated user\n submission_id (str): ID of the submission to be deleted\n\nReturns:\n bool: True if the submission was successfully deleted, False otherwise",
29922992
"operationId": "deleteV2Delete store submission",
29932993
"security": [{ "HTTPBearerJWT": [] }],
29942994
"parameters": [
@@ -3001,15 +3001,16 @@
30013001
],
30023002
"responses": {
30033003
"200": {
3004-
"description": "Submission deleted successfully",
3004+
"description": "Successful Response",
30053005
"content": {
30063006
"application/json": {
3007-
"schema": { "$ref": "#/components/schemas/StoreSubmission" }
3007+
"schema": {
3008+
"type": "boolean",
3009+
"title": "Response Deletev2Delete Store Submission"
3010+
}
30083011
}
30093012
}
30103013
},
3011-
"404": { "description": "Submission not found" },
3012-
"500": { "description": "Server error" },
30133014
"422": {
30143015
"description": "Validation Error",
30153016
"content": {
@@ -8119,6 +8120,11 @@
81198120
"title": "Node Exec Id",
81208121
"description": "Node execution ID to review"
81218122
},
8123+
"approved": {
8124+
"type": "boolean",
8125+
"title": "Approved",
8126+
"description": "Whether this review is approved (True) or rejected (False)"
8127+
},
81228128
"message": {
81238129
"anyOf": [
81248130
{ "type": "string", "maxLength": 2000 },
@@ -8138,34 +8144,27 @@
81388144
{ "type": "null" }
81398145
],
81408146
"title": "Reviewed Data",
8141-
"description": "Optional edited data"
8147+
"description": "Optional edited data (ignored if approved=False)"
81428148
}
81438149
},
81448150
"type": "object",
8145-
"required": ["node_exec_id"],
8151+
"required": ["node_exec_id", "approved"],
81468152
"title": "ReviewItem",
81478153
"description": "Single review item for processing."
81488154
},
81498155
"ReviewRequest": {
81508156
"properties": {
8151-
"approved_reviews": {
8157+
"reviews": {
81528158
"items": { "$ref": "#/components/schemas/ReviewItem" },
81538159
"type": "array",
8154-
"title": "Approved Reviews",
8155-
"description": "Reviews to approve with their data and messages",
8156-
"default": []
8157-
},
8158-
"rejected_review_ids": {
8159-
"items": { "type": "string" },
8160-
"type": "array",
8161-
"title": "Rejected Review Ids",
8162-
"description": "Node execution IDs of reviews to reject",
8163-
"default": []
8160+
"title": "Reviews",
8161+
"description": "All reviews with their approval status, data, and messages"
81648162
}
81658163
},
81668164
"type": "object",
8165+
"required": ["reviews"],
81678166
"title": "ReviewRequest",
8168-
"description": "Request model for processing ALL pending reviews for an execution.\n\nThis request must include ALL pending reviews for a graph execution.\nEach review will be either approved (with optional data modifications)\nor rejected. The execution will resume only after ALL reviews are processed."
8167+
"description": "Request model for processing ALL pending reviews for an execution.\n\nThis request must include ALL pending reviews for a graph execution.\nEach review will be either approved (with optional data modifications)\nor rejected (data ignored). The execution will resume only after ALL reviews are processed."
81698168
},
81708169
"ReviewResponse": {
81718170
"properties": {

0 commit comments

Comments
 (0)