mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-19 20:18:22 -05:00
Compare commits
11 Commits
feature/vi
...
feat/ai-ge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b86afaaed | ||
|
|
3485d2ae85 | ||
|
|
31d0ae19e0 | ||
|
|
d00e6efe9b | ||
|
|
610a659c52 | ||
|
|
4eac081cf9 | ||
|
|
eef7cf8cf8 | ||
|
|
7c90b16209 | ||
|
|
3ec2f1953f | ||
|
|
8d3dfe2cf8 | ||
|
|
7133580d43 |
@@ -218,6 +218,7 @@ async def save_agent_to_library(
|
||||
library_agents = await library_db.create_library_agent(
|
||||
graph=created_graph,
|
||||
user_id=user_id,
|
||||
sensitive_action_safe_mode=True,
|
||||
create_library_agents_for_sub_graphs=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -401,27 +401,10 @@ async def add_generated_agent_image(
|
||||
)
|
||||
|
||||
|
||||
def _initialize_graph_settings(graph: graph_db.GraphModel) -> GraphSettings:
|
||||
"""
|
||||
Initialize GraphSettings based on graph content.
|
||||
|
||||
Args:
|
||||
graph: The graph to analyze
|
||||
|
||||
Returns:
|
||||
GraphSettings with appropriate human_in_the_loop_safe_mode value
|
||||
"""
|
||||
if graph.has_human_in_the_loop:
|
||||
# Graph has HITL blocks - set safe mode to True by default
|
||||
return GraphSettings(human_in_the_loop_safe_mode=True)
|
||||
else:
|
||||
# Graph has no HITL blocks - keep None
|
||||
return GraphSettings(human_in_the_loop_safe_mode=None)
|
||||
|
||||
|
||||
async def create_library_agent(
|
||||
graph: graph_db.GraphModel,
|
||||
user_id: str,
|
||||
sensitive_action_safe_mode: bool = False,
|
||||
create_library_agents_for_sub_graphs: bool = True,
|
||||
) -> list[library_model.LibraryAgent]:
|
||||
"""
|
||||
@@ -430,6 +413,7 @@ async def create_library_agent(
|
||||
Args:
|
||||
agent: The agent/Graph to add to the library.
|
||||
user_id: The user to whom the agent will be added.
|
||||
sensitive_action_safe_mode: Whether sensitive action blocks require review.
|
||||
create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well.
|
||||
|
||||
Returns:
|
||||
@@ -465,7 +449,10 @@ async def create_library_agent(
|
||||
}
|
||||
},
|
||||
settings=SafeJson(
|
||||
_initialize_graph_settings(graph_entry).model_dump()
|
||||
GraphSettings.from_graph(
|
||||
graph_entry,
|
||||
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
||||
).model_dump()
|
||||
),
|
||||
),
|
||||
include=library_agent_include(
|
||||
@@ -627,33 +614,6 @@ async def update_library_agent(
|
||||
raise DatabaseError("Failed to update library agent") from e
|
||||
|
||||
|
||||
async def update_library_agent_settings(
|
||||
user_id: str,
|
||||
agent_id: str,
|
||||
settings: GraphSettings,
|
||||
) -> library_model.LibraryAgent:
|
||||
"""
|
||||
Updates the settings for a specific LibraryAgent.
|
||||
|
||||
Args:
|
||||
user_id: The owner of the LibraryAgent.
|
||||
agent_id: The ID of the LibraryAgent to update.
|
||||
settings: New GraphSettings to apply.
|
||||
|
||||
Returns:
|
||||
The updated LibraryAgent.
|
||||
|
||||
Raises:
|
||||
NotFoundError: If the specified LibraryAgent does not exist.
|
||||
DatabaseError: If there's an error in the update operation.
|
||||
"""
|
||||
return await update_library_agent(
|
||||
library_agent_id=agent_id,
|
||||
user_id=user_id,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
|
||||
async def delete_library_agent(
|
||||
library_agent_id: str, user_id: str, soft_delete: bool = True
|
||||
) -> None:
|
||||
@@ -838,7 +798,7 @@ async def add_store_agent_to_library(
|
||||
"isCreatedByUser": False,
|
||||
"useGraphIsActiveVersion": False,
|
||||
"settings": SafeJson(
|
||||
_initialize_graph_settings(graph_model).model_dump()
|
||||
GraphSettings.from_graph(graph_model).model_dump()
|
||||
),
|
||||
},
|
||||
include=library_agent_include(
|
||||
@@ -1228,8 +1188,14 @@ async def fork_library_agent(
|
||||
)
|
||||
new_graph = await on_graph_activate(new_graph, user_id=user_id)
|
||||
|
||||
# Create a library agent for the new graph
|
||||
return (await create_library_agent(new_graph, user_id))[0]
|
||||
# Create a library agent for the new graph, preserving sensitive_action_safe_mode
|
||||
return (
|
||||
await create_library_agent(
|
||||
new_graph,
|
||||
user_id,
|
||||
sensitive_action_safe_mode=original_agent.settings.sensitive_action_safe_mode,
|
||||
)
|
||||
)[0]
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error cloning library agent: {e}")
|
||||
raise DatabaseError("Failed to fork library agent") from e
|
||||
|
||||
@@ -73,6 +73,12 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
has_external_trigger: bool = pydantic.Field(
|
||||
description="Whether the agent has an external trigger (e.g. webhook) node"
|
||||
)
|
||||
has_human_in_the_loop: bool = pydantic.Field(
|
||||
description="Whether the agent has human-in-the-loop blocks"
|
||||
)
|
||||
has_sensitive_action: bool = pydantic.Field(
|
||||
description="Whether the agent has sensitive action blocks"
|
||||
)
|
||||
trigger_setup_info: Optional[GraphTriggerInfo] = None
|
||||
|
||||
# Indicates whether there's a new output (based on recent runs)
|
||||
@@ -180,6 +186,8 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
graph.credentials_input_schema if sub_graphs is not None else None
|
||||
),
|
||||
has_external_trigger=graph.has_external_trigger,
|
||||
has_human_in_the_loop=graph.has_human_in_the_loop,
|
||||
has_sensitive_action=graph.has_sensitive_action,
|
||||
trigger_setup_info=graph.trigger_setup_info,
|
||||
new_output=new_output,
|
||||
can_access_graph=can_access_graph,
|
||||
|
||||
@@ -52,6 +52,8 @@ async def test_get_library_agents_success(
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
credentials_input_schema={"type": "object", "properties": {}},
|
||||
has_external_trigger=False,
|
||||
has_human_in_the_loop=False,
|
||||
has_sensitive_action=False,
|
||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||
recommended_schedule_cron=None,
|
||||
new_output=False,
|
||||
@@ -75,6 +77,8 @@ async def test_get_library_agents_success(
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
credentials_input_schema={"type": "object", "properties": {}},
|
||||
has_external_trigger=False,
|
||||
has_human_in_the_loop=False,
|
||||
has_sensitive_action=False,
|
||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||
recommended_schedule_cron=None,
|
||||
new_output=False,
|
||||
@@ -150,6 +154,8 @@ async def test_get_favorite_library_agents_success(
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
credentials_input_schema={"type": "object", "properties": {}},
|
||||
has_external_trigger=False,
|
||||
has_human_in_the_loop=False,
|
||||
has_sensitive_action=False,
|
||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||
recommended_schedule_cron=None,
|
||||
new_output=False,
|
||||
@@ -218,6 +224,8 @@ def test_add_agent_to_library_success(
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
credentials_input_schema={"type": "object", "properties": {}},
|
||||
has_external_trigger=False,
|
||||
has_human_in_the_loop=False,
|
||||
has_sensitive_action=False,
|
||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||
new_output=False,
|
||||
can_access_graph=True,
|
||||
|
||||
@@ -761,10 +761,8 @@ async def create_new_graph(
|
||||
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
||||
graph.validate_graph(for_run=False)
|
||||
|
||||
# The return value of the create graph & library function is intentionally not used here,
|
||||
# as the graph already valid and no sub-graphs are returned back.
|
||||
await graph_db.create_graph(graph, user_id=user_id)
|
||||
await library_db.create_library_agent(graph, user_id=user_id)
|
||||
await library_db.create_library_agent(graph, user_id)
|
||||
activated_graph = await on_graph_activate(graph, user_id=user_id)
|
||||
|
||||
if create_graph.source == "builder":
|
||||
@@ -888,21 +886,19 @@ async def set_graph_active_version(
|
||||
async def _update_library_agent_version_and_settings(
|
||||
user_id: str, agent_graph: graph_db.GraphModel
|
||||
) -> library_model.LibraryAgent:
|
||||
# Keep the library agent up to date with the new active version
|
||||
library = await library_db.update_agent_version_in_library(
|
||||
user_id, agent_graph.id, agent_graph.version
|
||||
)
|
||||
# If the graph has HITL node, initialize the setting if it's not already set.
|
||||
if (
|
||||
agent_graph.has_human_in_the_loop
|
||||
and library.settings.human_in_the_loop_safe_mode is None
|
||||
):
|
||||
await library_db.update_library_agent_settings(
|
||||
updated_settings = GraphSettings.from_graph(
|
||||
graph=agent_graph,
|
||||
hitl_safe_mode=library.settings.human_in_the_loop_safe_mode,
|
||||
sensitive_action_safe_mode=library.settings.sensitive_action_safe_mode,
|
||||
)
|
||||
if updated_settings != library.settings:
|
||||
library = await library_db.update_library_agent(
|
||||
library_agent_id=library.id,
|
||||
user_id=user_id,
|
||||
agent_id=library.id,
|
||||
settings=library.settings.model_copy(
|
||||
update={"human_in_the_loop_safe_mode": True}
|
||||
),
|
||||
settings=updated_settings,
|
||||
)
|
||||
return library
|
||||
|
||||
@@ -919,21 +915,18 @@ async def update_graph_settings(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> GraphSettings:
|
||||
"""Update graph settings for the user's library agent."""
|
||||
# Get the library agent for this graph
|
||||
library_agent = await library_db.get_library_agent_by_graph_id(
|
||||
graph_id=graph_id, user_id=user_id
|
||||
)
|
||||
if not library_agent:
|
||||
raise HTTPException(404, f"Graph #{graph_id} not found in user's library")
|
||||
|
||||
# Update the library agent settings
|
||||
updated_agent = await library_db.update_library_agent_settings(
|
||||
updated_agent = await library_db.update_library_agent(
|
||||
library_agent_id=library_agent.id,
|
||||
user_id=user_id,
|
||||
agent_id=library_agent.id,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
# Return the updated settings
|
||||
return GraphSettings.model_validate(updated_agent.settings)
|
||||
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class HITLReviewHelper:
|
||||
Exception: If review creation or status update fails
|
||||
"""
|
||||
# Skip review if safe mode is disabled - return auto-approved result
|
||||
if not execution_context.safe_mode:
|
||||
if not execution_context.human_in_the_loop_safe_mode:
|
||||
logger.info(
|
||||
f"Block {block_name} skipping review for node {node_exec_id} - safe mode disabled"
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ class HumanInTheLoopBlock(Block):
|
||||
execution_context: ExecutionContext,
|
||||
**_kwargs,
|
||||
) -> BlockOutput:
|
||||
if not execution_context.safe_mode:
|
||||
if not execution_context.human_in_the_loop_safe_mode:
|
||||
logger.info(
|
||||
f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
|
||||
)
|
||||
|
||||
@@ -242,7 +242,7 @@ async def test_smart_decision_maker_tracks_llm_stats():
|
||||
outputs = {}
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -343,7 +343,7 @@ async def test_smart_decision_maker_parameter_validation():
|
||||
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -409,7 +409,7 @@ async def test_smart_decision_maker_parameter_validation():
|
||||
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -471,7 +471,7 @@ async def test_smart_decision_maker_parameter_validation():
|
||||
outputs = {}
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -535,7 +535,7 @@ async def test_smart_decision_maker_parameter_validation():
|
||||
outputs = {}
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -658,7 +658,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
||||
outputs = {}
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -730,7 +730,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
||||
outputs = {}
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -786,7 +786,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
||||
outputs = {}
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
@@ -905,7 +905,7 @@ async def test_smart_decision_maker_agent_mode():
|
||||
# Create a mock execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(
|
||||
safe_mode=False,
|
||||
human_in_the_loop_safe_mode=False,
|
||||
)
|
||||
|
||||
# Create a mock execution processor for agent mode tests
|
||||
@@ -1027,7 +1027,7 @@ async def test_smart_decision_maker_traditional_mode_default():
|
||||
|
||||
# Create execution context
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
|
||||
# Create a mock execution processor for tests
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ async def test_output_yielding_with_dynamic_fields():
|
||||
outputs = {}
|
||||
from backend.data.execution import ExecutionContext
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||
mock_execution_processor = MagicMock()
|
||||
|
||||
async for output_name, output_value in block.run(
|
||||
@@ -609,7 +609,9 @@ async def test_validation_errors_dont_pollute_conversation():
|
||||
outputs = {}
|
||||
from backend.data.execution import ExecutionContext
|
||||
|
||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||
mock_execution_context = ExecutionContext(
|
||||
human_in_the_loop_safe_mode=False
|
||||
)
|
||||
|
||||
# Create a proper mock execution processor for agent mode
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -474,7 +474,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
||||
self.block_type = block_type
|
||||
self.webhook_config = webhook_config
|
||||
self.execution_stats: NodeExecutionStats = NodeExecutionStats()
|
||||
self.requires_human_review: bool = False
|
||||
self.is_sensitive_action: bool = False
|
||||
|
||||
if self.webhook_config:
|
||||
if isinstance(self.webhook_config, BlockWebhookConfig):
|
||||
@@ -637,8 +637,9 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
||||
- should_pause: True if execution should be paused for review
|
||||
- input_data_to_use: The input data to use (may be modified by reviewer)
|
||||
"""
|
||||
# Skip review if not required or safe mode is disabled
|
||||
if not self.requires_human_review or not execution_context.safe_mode:
|
||||
if not (
|
||||
self.is_sensitive_action and execution_context.sensitive_action_safe_mode
|
||||
):
|
||||
return False, input_data
|
||||
|
||||
from backend.blocks.helpers.review import HITLReviewHelper
|
||||
|
||||
@@ -81,7 +81,8 @@ class ExecutionContext(BaseModel):
|
||||
This includes information needed by blocks, sub-graphs, and execution management.
|
||||
"""
|
||||
|
||||
safe_mode: bool = True
|
||||
human_in_the_loop_safe_mode: bool = True
|
||||
sensitive_action_safe_mode: bool = False
|
||||
user_timezone: str = "UTC"
|
||||
root_execution_id: Optional[str] = None
|
||||
parent_execution_id: Optional[str] = None
|
||||
|
||||
@@ -62,7 +62,23 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GraphSettings(BaseModel):
|
||||
human_in_the_loop_safe_mode: bool | None = None
|
||||
human_in_the_loop_safe_mode: bool = True
|
||||
sensitive_action_safe_mode: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_graph(
|
||||
cls,
|
||||
graph: "GraphModel",
|
||||
hitl_safe_mode: bool | None = None,
|
||||
sensitive_action_safe_mode: bool = False,
|
||||
) -> "GraphSettings":
|
||||
# Default to True if not explicitly set
|
||||
if hitl_safe_mode is None:
|
||||
hitl_safe_mode = True
|
||||
return cls(
|
||||
human_in_the_loop_safe_mode=hitl_safe_mode,
|
||||
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
||||
)
|
||||
|
||||
|
||||
class Link(BaseDbModel):
|
||||
@@ -244,10 +260,14 @@ class BaseGraph(BaseDbModel):
|
||||
return any(
|
||||
node.block_id
|
||||
for node in self.nodes
|
||||
if (
|
||||
node.block.block_type == BlockType.HUMAN_IN_THE_LOOP
|
||||
or node.block.requires_human_review
|
||||
)
|
||||
if node.block.block_type == BlockType.HUMAN_IN_THE_LOOP
|
||||
)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def has_sensitive_action(self) -> bool:
|
||||
return any(
|
||||
node.block_id for node in self.nodes if node.block.is_sensitive_action
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -873,11 +873,8 @@ async def add_graph_execution(
|
||||
settings = await gdb.get_graph_settings(user_id=user_id, graph_id=graph_id)
|
||||
|
||||
execution_context = ExecutionContext(
|
||||
safe_mode=(
|
||||
settings.human_in_the_loop_safe_mode
|
||||
if settings.human_in_the_loop_safe_mode is not None
|
||||
else True
|
||||
),
|
||||
human_in_the_loop_safe_mode=settings.human_in_the_loop_safe_mode,
|
||||
sensitive_action_safe_mode=settings.sensitive_action_safe_mode,
|
||||
user_timezone=(
|
||||
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
|
||||
),
|
||||
|
||||
@@ -386,6 +386,7 @@ async def test_add_graph_execution_is_repeatable(mocker: MockerFixture):
|
||||
mock_user.timezone = "UTC"
|
||||
mock_settings = mocker.MagicMock()
|
||||
mock_settings.human_in_the_loop_safe_mode = True
|
||||
mock_settings.sensitive_action_safe_mode = False
|
||||
|
||||
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
||||
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
||||
@@ -651,6 +652,7 @@ async def test_add_graph_execution_with_nodes_to_skip(mocker: MockerFixture):
|
||||
mock_user.timezone = "UTC"
|
||||
mock_settings = mocker.MagicMock()
|
||||
mock_settings.human_in_the_loop_safe_mode = True
|
||||
mock_settings.sensitive_action_safe_mode = False
|
||||
|
||||
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
||||
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"forked_from_version": null,
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"has_sensitive_action": false,
|
||||
"id": "graph-123",
|
||||
"input_schema": {
|
||||
"properties": {},
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"forked_from_version": null,
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"has_sensitive_action": false,
|
||||
"id": "graph-123",
|
||||
"input_schema": {
|
||||
"properties": {},
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"properties": {}
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"has_sensitive_action": false,
|
||||
"trigger_setup_info": null,
|
||||
"new_output": false,
|
||||
"can_access_graph": true,
|
||||
@@ -34,7 +36,8 @@
|
||||
"is_favorite": false,
|
||||
"recommended_schedule_cron": null,
|
||||
"settings": {
|
||||
"human_in_the_loop_safe_mode": null
|
||||
"human_in_the_loop_safe_mode": true,
|
||||
"sensitive_action_safe_mode": false
|
||||
},
|
||||
"marketplace_listing": null
|
||||
},
|
||||
@@ -65,6 +68,8 @@
|
||||
"properties": {}
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"has_sensitive_action": false,
|
||||
"trigger_setup_info": null,
|
||||
"new_output": false,
|
||||
"can_access_graph": false,
|
||||
@@ -72,7 +77,8 @@
|
||||
"is_favorite": false,
|
||||
"recommended_schedule_cron": null,
|
||||
"settings": {
|
||||
"human_in_the_loop_safe_mode": null
|
||||
"human_in_the_loop_safe_mode": true,
|
||||
"sensitive_action_safe_mode": false
|
||||
},
|
||||
"marketplace_listing": null
|
||||
}
|
||||
|
||||
@@ -31,10 +31,18 @@ export function AgentSettingsModal({
|
||||
}
|
||||
}
|
||||
|
||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||
useAgentSafeMode(agent);
|
||||
const {
|
||||
currentHITLSafeMode,
|
||||
showHITLToggle,
|
||||
handleHITLToggle,
|
||||
currentSensitiveActionSafeMode,
|
||||
showSensitiveActionToggle,
|
||||
handleSensitiveActionToggle,
|
||||
isPending,
|
||||
shouldShowToggle,
|
||||
} = useAgentSafeMode(agent);
|
||||
|
||||
if (!hasHITLBlocks) return null;
|
||||
if (!shouldShowToggle) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -57,23 +65,48 @@ export function AgentSettingsModal({
|
||||
)}
|
||||
<Dialog.Content>
|
||||
<div className="space-y-6">
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">Require human approval</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause and wait for your review before
|
||||
continuing
|
||||
</Text>
|
||||
{showHITLToggle && (
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">
|
||||
Human-in-the-loop approval
|
||||
</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause at human-in-the-loop blocks and wait
|
||||
for your review before continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentHITLSafeMode || false}
|
||||
onCheckedChange={handleHITLToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSafeMode || false}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showSensitiveActionToggle && (
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">
|
||||
Sensitive action approval
|
||||
</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause at sensitive action blocks and wait for
|
||||
your review before continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSensitiveActionSafeMode}
|
||||
onCheckedChange={handleSensitiveActionToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,8 +13,16 @@ interface Props {
|
||||
}
|
||||
|
||||
export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||
useAgentSafeMode(agent);
|
||||
const {
|
||||
currentHITLSafeMode,
|
||||
showHITLToggle,
|
||||
handleHITLToggle,
|
||||
currentSensitiveActionSafeMode,
|
||||
showSensitiveActionToggle,
|
||||
handleSensitiveActionToggle,
|
||||
isPending,
|
||||
shouldShowToggle,
|
||||
} = useAgentSafeMode(agent);
|
||||
|
||||
return (
|
||||
<SelectedViewLayout agent={agent}>
|
||||
@@ -34,24 +42,51 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
</div>
|
||||
|
||||
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
|
||||
{hasHITLBlocks ? (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">Require human approval</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause and wait for your review before
|
||||
continuing
|
||||
</Text>
|
||||
{shouldShowToggle ? (
|
||||
<>
|
||||
{showHITLToggle && (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">
|
||||
Human-in-the-loop approval
|
||||
</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause at human-in-the-loop blocks and
|
||||
wait for your review before continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentHITLSafeMode || false}
|
||||
onCheckedChange={handleHITLToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSafeMode || false}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showSensitiveActionToggle && (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">
|
||||
Sensitive action approval
|
||||
</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause at sensitive action blocks and wait
|
||||
for your review before continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSensitiveActionSafeMode}
|
||||
onCheckedChange={handleSensitiveActionToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
|
||||
@@ -6383,6 +6383,11 @@
|
||||
"title": "Has Human In The Loop",
|
||||
"readOnly": true
|
||||
},
|
||||
"has_sensitive_action": {
|
||||
"type": "boolean",
|
||||
"title": "Has Sensitive Action",
|
||||
"readOnly": true
|
||||
},
|
||||
"trigger_setup_info": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||
@@ -6399,6 +6404,7 @@
|
||||
"output_schema",
|
||||
"has_external_trigger",
|
||||
"has_human_in_the_loop",
|
||||
"has_sensitive_action",
|
||||
"trigger_setup_info"
|
||||
],
|
||||
"title": "BaseGraph"
|
||||
@@ -7603,6 +7609,11 @@
|
||||
"title": "Has Human In The Loop",
|
||||
"readOnly": true
|
||||
},
|
||||
"has_sensitive_action": {
|
||||
"type": "boolean",
|
||||
"title": "Has Sensitive Action",
|
||||
"readOnly": true
|
||||
},
|
||||
"trigger_setup_info": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||
@@ -7626,6 +7637,7 @@
|
||||
"output_schema",
|
||||
"has_external_trigger",
|
||||
"has_human_in_the_loop",
|
||||
"has_sensitive_action",
|
||||
"trigger_setup_info",
|
||||
"credentials_input_schema"
|
||||
],
|
||||
@@ -7704,6 +7716,11 @@
|
||||
"title": "Has Human In The Loop",
|
||||
"readOnly": true
|
||||
},
|
||||
"has_sensitive_action": {
|
||||
"type": "boolean",
|
||||
"title": "Has Sensitive Action",
|
||||
"readOnly": true
|
||||
},
|
||||
"trigger_setup_info": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||
@@ -7728,6 +7745,7 @@
|
||||
"output_schema",
|
||||
"has_external_trigger",
|
||||
"has_human_in_the_loop",
|
||||
"has_sensitive_action",
|
||||
"trigger_setup_info",
|
||||
"credentials_input_schema"
|
||||
],
|
||||
@@ -7736,8 +7754,14 @@
|
||||
"GraphSettings": {
|
||||
"properties": {
|
||||
"human_in_the_loop_safe_mode": {
|
||||
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
|
||||
"title": "Human In The Loop Safe Mode"
|
||||
"type": "boolean",
|
||||
"title": "Human In The Loop Safe Mode",
|
||||
"default": true
|
||||
},
|
||||
"sensitive_action_safe_mode": {
|
||||
"type": "boolean",
|
||||
"title": "Sensitive Action Safe Mode",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -7895,6 +7919,16 @@
|
||||
"title": "Has External Trigger",
|
||||
"description": "Whether the agent has an external trigger (e.g. webhook) node"
|
||||
},
|
||||
"has_human_in_the_loop": {
|
||||
"type": "boolean",
|
||||
"title": "Has Human In The Loop",
|
||||
"description": "Whether the agent has human-in-the-loop blocks"
|
||||
},
|
||||
"has_sensitive_action": {
|
||||
"type": "boolean",
|
||||
"title": "Has Sensitive Action",
|
||||
"description": "Whether the agent has sensitive action blocks"
|
||||
},
|
||||
"trigger_setup_info": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||
@@ -7941,6 +7975,8 @@
|
||||
"output_schema",
|
||||
"credentials_input_schema",
|
||||
"has_external_trigger",
|
||||
"has_human_in_the_loop",
|
||||
"has_sensitive_action",
|
||||
"new_output",
|
||||
"can_access_graph",
|
||||
"is_latest_version",
|
||||
|
||||
@@ -20,11 +20,15 @@ function hasHITLBlocks(graph: GraphModel | LibraryAgent | Graph): boolean {
|
||||
if ("has_human_in_the_loop" in graph) {
|
||||
return !!graph.has_human_in_the_loop;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isLibraryAgent(graph)) {
|
||||
return graph.settings?.human_in_the_loop_safe_mode !== null;
|
||||
function hasSensitiveActionBlocks(
|
||||
graph: GraphModel | LibraryAgent | Graph,
|
||||
): boolean {
|
||||
if ("has_sensitive_action" in graph) {
|
||||
return !!graph.has_sensitive_action;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -40,7 +44,9 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
||||
|
||||
const graphId = getGraphId(graph);
|
||||
const isAgent = isLibraryAgent(graph);
|
||||
const shouldShowToggle = hasHITLBlocks(graph);
|
||||
const showHITLToggle = hasHITLBlocks(graph);
|
||||
const showSensitiveActionToggle = hasSensitiveActionBlocks(graph);
|
||||
const shouldShowToggle = showHITLToggle || showSensitiveActionToggle;
|
||||
|
||||
const { mutateAsync: updateGraphSettings, isPending } =
|
||||
usePatchV1UpdateGraphSettings();
|
||||
@@ -56,27 +62,37 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
||||
},
|
||||
);
|
||||
|
||||
const [localSafeMode, setLocalSafeMode] = useState<boolean | null>(null);
|
||||
const [localHITLSafeMode, setLocalHITLSafeMode] = useState<boolean>(true);
|
||||
const [localSensitiveActionSafeMode, setLocalSensitiveActionSafeMode] =
|
||||
useState<boolean>(false);
|
||||
const [isLocalStateLoaded, setIsLocalStateLoaded] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAgent && libraryAgent) {
|
||||
const backendValue = libraryAgent.settings?.human_in_the_loop_safe_mode;
|
||||
if (backendValue !== undefined) {
|
||||
setLocalSafeMode(backendValue);
|
||||
}
|
||||
setLocalHITLSafeMode(
|
||||
libraryAgent.settings?.human_in_the_loop_safe_mode ?? true,
|
||||
);
|
||||
setLocalSensitiveActionSafeMode(
|
||||
libraryAgent.settings?.sensitive_action_safe_mode ?? false,
|
||||
);
|
||||
setIsLocalStateLoaded(true);
|
||||
}
|
||||
}, [isAgent, libraryAgent]);
|
||||
|
||||
const currentSafeMode = isAgent
|
||||
? graph.settings?.human_in_the_loop_safe_mode
|
||||
: localSafeMode;
|
||||
const currentHITLSafeMode = isAgent
|
||||
? (graph.settings?.human_in_the_loop_safe_mode ?? true)
|
||||
: localHITLSafeMode;
|
||||
|
||||
const isStateUndetermined = isAgent
|
||||
? graph.settings?.human_in_the_loop_safe_mode == null
|
||||
: isLoading || localSafeMode === null;
|
||||
const currentSensitiveActionSafeMode = isAgent
|
||||
? (graph.settings?.sensitive_action_safe_mode ?? false)
|
||||
: localSensitiveActionSafeMode;
|
||||
|
||||
const handleToggle = useCallback(async () => {
|
||||
const newSafeMode = !currentSafeMode;
|
||||
const isHITLStateUndetermined = isAgent
|
||||
? false
|
||||
: isLoading || !isLocalStateLoaded;
|
||||
|
||||
const handleHITLToggle = useCallback(async () => {
|
||||
const newSafeMode = !currentHITLSafeMode;
|
||||
|
||||
try {
|
||||
await updateGraphSettings({
|
||||
@@ -85,7 +101,7 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
||||
});
|
||||
|
||||
if (!isAgent) {
|
||||
setLocalSafeMode(newSafeMode);
|
||||
setLocalHITLSafeMode(newSafeMode);
|
||||
}
|
||||
|
||||
if (isAgent) {
|
||||
@@ -101,37 +117,62 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
||||
queryClient.invalidateQueries({ queryKey: ["v2", "executions"] });
|
||||
|
||||
toast({
|
||||
title: `Safe mode ${newSafeMode ? "enabled" : "disabled"}`,
|
||||
title: `HITL safe mode ${newSafeMode ? "enabled" : "disabled"}`,
|
||||
description: newSafeMode
|
||||
? "Human-in-the-loop blocks will require manual review"
|
||||
: "Human-in-the-loop blocks will proceed automatically",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
const isNotFoundError =
|
||||
error instanceof Error &&
|
||||
(error.message.includes("404") || error.message.includes("not found"));
|
||||
|
||||
if (!isAgent && isNotFoundError) {
|
||||
toast({
|
||||
title: "Safe mode not available",
|
||||
description:
|
||||
"To configure safe mode, please save this graph to your library first.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Failed to update safe mode",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
handleToggleError(error, isAgent, toast);
|
||||
}
|
||||
}, [
|
||||
currentSafeMode,
|
||||
currentHITLSafeMode,
|
||||
graphId,
|
||||
isAgent,
|
||||
graph.id,
|
||||
updateGraphSettings,
|
||||
queryClient,
|
||||
toast,
|
||||
]);
|
||||
|
||||
const handleSensitiveActionToggle = useCallback(async () => {
|
||||
const newSafeMode = !currentSensitiveActionSafeMode;
|
||||
|
||||
try {
|
||||
await updateGraphSettings({
|
||||
graphId,
|
||||
data: { sensitive_action_safe_mode: newSafeMode },
|
||||
});
|
||||
|
||||
if (!isAgent) {
|
||||
setLocalSensitiveActionSafeMode(newSafeMode);
|
||||
}
|
||||
|
||||
if (isAgent) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2GetLibraryAgentQueryOptions(graph.id.toString())
|
||||
.queryKey,
|
||||
});
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["v1", "graphs", graphId, "executions"],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["v2", "executions"] });
|
||||
|
||||
toast({
|
||||
title: `Sensitive action safe mode ${newSafeMode ? "enabled" : "disabled"}`,
|
||||
description: newSafeMode
|
||||
? "Sensitive action blocks will require manual review"
|
||||
: "Sensitive action blocks will proceed automatically",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
handleToggleError(error, isAgent, toast);
|
||||
}
|
||||
}, [
|
||||
currentSensitiveActionSafeMode,
|
||||
graphId,
|
||||
isAgent,
|
||||
graph.id,
|
||||
@@ -141,11 +182,53 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
||||
]);
|
||||
|
||||
return {
|
||||
currentSafeMode,
|
||||
// HITL safe mode
|
||||
currentHITLSafeMode,
|
||||
showHITLToggle,
|
||||
isHITLStateUndetermined,
|
||||
handleHITLToggle,
|
||||
|
||||
// Sensitive action safe mode
|
||||
currentSensitiveActionSafeMode,
|
||||
showSensitiveActionToggle,
|
||||
handleSensitiveActionToggle,
|
||||
|
||||
// General
|
||||
isPending,
|
||||
shouldShowToggle,
|
||||
isStateUndetermined,
|
||||
handleToggle,
|
||||
hasHITLBlocks: shouldShowToggle,
|
||||
|
||||
// Backwards compatibility
|
||||
currentSafeMode: currentHITLSafeMode,
|
||||
isStateUndetermined: isHITLStateUndetermined,
|
||||
handleToggle: handleHITLToggle,
|
||||
hasHITLBlocks: showHITLToggle,
|
||||
};
|
||||
}
|
||||
|
||||
function handleToggleError(
|
||||
error: unknown,
|
||||
isAgent: boolean,
|
||||
toast: ReturnType<typeof useToast>["toast"],
|
||||
) {
|
||||
const isNotFoundError =
|
||||
error instanceof Error &&
|
||||
(error.message.includes("404") || error.message.includes("not found"));
|
||||
|
||||
if (!isAgent && isNotFoundError) {
|
||||
toast({
|
||||
title: "Safe mode not available",
|
||||
description:
|
||||
"To configure safe mode, please save this graph to your library first.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Failed to update safe mode",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user