Compare commits

..

11 Commits

Author SHA1 Message Date
Zamil Majdy
83e707cc7e fix(frontend): Remove trailing newline from package.json for prettier 2026-01-21 12:43:07 -05:00
Zamil Majdy
50994d644e fix(backend): Install pgvector in current schema for CI compatibility
- Remove hardcoded 'WITH SCHEMA public' to install in current schema
- Use unqualified vector type in migration (vector(1536) not public.vector)
- Ensures pgvector is in same schema as tables for search_path resolution
- Fixes 'type vector does not exist' errors in CI tests
2026-01-21 12:21:04 -05:00
Zamil Majdy
aec2e33605 chore: Trigger frontend CI for e2e marketplace tests 2026-01-21 11:53:25 -05:00
Zamil Majdy
6ed8b23f27 chore: Trigger frontend CI for e2e testing 2026-01-21 11:47:19 -05:00
Zamil Majdy
731c29dcea revert: Keep migration unchanged from dev branch 2026-01-21 11:36:34 -05:00
Zamil Majdy
3df7ebe2fc Merge dev and fix pgvector schema: use unqualified vector type
Resolve merge conflicts by using unqualified ::vector type cast and
<=> operator instead of {pgvector_schema} placeholder.

The dev branch added {pgvector_schema} which defaults to "public", but
pgvector is installed in different schemas depending on environment:
- Local: platform schema
- Dev (Supabase): platform schema
- CI: public schema

Using unqualified ::vector works on all environments because PostgreSQL
resolves types via search_path, which includes the schema where pgvector
is installed.

Fixes: AUTOGPT-SERVER-763, AUTOGPT-SERVER-764, AUTOGPT-SERVER-76B
2026-01-21 11:32:58 -05:00
Zamil Majdy
f23c350da1 fix(backend): Use unqualified vector type for pgvector queries
Remove explicit schema qualification ({schema}.vector and
OPERATOR({schema}.<=>)) from pgvector queries. The vector type
and <=> operator work unqualified because pgvector is installed
in a schema that's part of the search_path on all environments.

This fixes "type 'platform.vector' does not exist" errors in CI
where pgvector is in public schema, and "type 'public.vector'
does not exist" errors on dev where pgvector is in platform schema.

Fixes: AUTOGPT-SERVER-763, AUTOGPT-SERVER-764, AUTOGPT-SERVER-76B
2026-01-21 11:27:03 -05:00
Zamil Majdy
033f58c075 fix(backend): Make Redis event bus gracefully handle connection failures (#11817)
## Summary
Adds graceful error handling to AsyncRedisEventBus and RedisEventBus so
that connection failures log exceptions with full traceback while
remaining non-breaking. This allows DatabaseManager to operate without
Redis connectivity.

## Problem
DatabaseManager was failing with "Authentication required" when trying
to publish notifications via AsyncRedisNotificationEventBus. The service
has no Redis credentials configured, causing `increment_onboarding_runs`
to fail.

## Root Cause
When `increment_onboarding_runs` publishes a notification:
1. Calls `AsyncRedisNotificationEventBus().publish()`
2. Attempts to connect to Redis via `get_redis_async()`
3. Connection fails due to missing credentials
4. Exception propagates, failing the entire DB operation

Previous fix (#11775) made the cache module lazy, but didn't address the
notification bus which also requires Redis.

## Solution
Wrap Redis operations in try-except blocks:
- `publish_event`: Logs exception with traceback, continues without
publishing
- `listen_events`: Logs exception with traceback, returns empty
generator
- `wait_for_event`: Returns None on connection failure

Using `logger.exception()` instead of `logger.warning()` ensures full
stack traces are captured for debugging while keeping operations
non-breaking.

This allows services to operate without Redis when only using event bus
for non-critical notifications.

## Changes
- Modified `backend/data/event_bus.py`:
- Added graceful error handling to `RedisEventBus` and
`AsyncRedisEventBus`
- All Redis operations now catch exceptions and log with
`logger.exception()`
- Added `backend/data/event_bus_test.py`:
  - Tests verify graceful degradation when Redis is unavailable
  - Tests verify normal operation when Redis is available

## Test Plan
- [x] New tests verify graceful degradation when Redis unavailable
- [x] Existing notification tests still pass
- [x] DatabaseManager can increment onboarding runs without Redis

## Related Issues
Fixes https://significant-gravitas.sentry.io/issues/7205834440/
(AUTOGPT-SERVER-76D)
2026-01-21 15:51:26 +00:00
Ubbe
40ef2d511f fix(frontend): auto-select credentials correctly in old builder (#11815)
## Changes 🏗️

On the **Old Builder**, when running an agent...

### Before

<img width="800" height="614" alt="Screenshot 2026-01-21 at 21 27 05"
src="https://github.com/user-attachments/assets/a3b2ec17-597f-44d2-9130-9e7931599c38"
/>

Credentials are there, but it is not recognising them, you need to click
on them to be selected

### After

<img width="1029" height="728" alt="Screenshot 2026-01-21 at 21 26 47"
src="https://github.com/user-attachments/assets/c6e83846-6048-439e-919d-6807674f2d5a"
/>

It uses the new credentials UI and correctly auto-selects existing ones.

### Other

Fixed a small timezone display glitch on the new library view.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run agent in old builder
- [x] Credentials are auto-selected and using the new collapsed system
credentials UI
2026-01-21 14:55:49 +00:00
Zamil Majdy
0a48c49902 test(backend): Update test to match removed SET search_path call
The SET search_path call was removed in favor of explicit schema
qualification, so the test now expects only 1 execute_raw call.
2026-01-19 21:31:40 -05:00
Zamil Majdy
1fc1102eb4 fix(backend): Use explicit schema qualification for pgvector types
Fix intermittent "type 'vector' does not exist" errors when using
PgBouncer in transaction mode. The issue was that SET search_path
and the actual query could run on different backend connections.

Changes:
- Add {schema} placeholder for raw schema name (e.g., platform)
- Use explicit type casting: {schema}.vector instead of ::vector
- Use explicit operator: OPERATOR({schema}.<=>) instead of <=>
- Remove set_public_search_path parameter (no longer needed)
- Remove search_path manipulation from DATABASE_URL

Tested on both local and dev environments via kubectl exec.

Fixes: AUTOGPT-SERVER-763, AUTOGPT-SERVER-764
2026-01-19 18:14:29 -05:00
30 changed files with 335 additions and 374 deletions

View File

@@ -41,7 +41,6 @@ class PendingHumanReviewModel(BaseModel):
graph_exec_id: str = Field(description="Graph execution ID")
graph_id: str = Field(description="Graph ID")
graph_version: int = Field(description="Graph version")
node_id: str = Field(description="Node ID in the graph definition")
payload: SafeJsonData = Field(description="The actual data payload awaiting review")
instructions: str | None = Field(
description="Instructions or message for the reviewer", default=None
@@ -82,7 +81,6 @@ class PendingHumanReviewModel(BaseModel):
graph_exec_id=review.graphExecId,
graph_id=review.graphId,
graph_version=review.graphVersion,
node_id=review.nodeId,
payload=review.payload,
instructions=review.instructions,
editable=review.editable,
@@ -181,15 +179,6 @@ class ReviewRequest(BaseModel):
reviews: List[ReviewItem] = Field(
description="All reviews with their approval status, data, and messages"
)
auto_approve_node_ids: List[str] = Field(
default_factory=list,
description=(
"List of node IDs (from the graph definition) to auto-approve for "
"the remainder of this execution. Future reviews from these specific "
"nodes will be automatically approved. This only affects the current "
"execution run."
),
)
@model_validator(mode="after")
def validate_review_completeness(self):

View File

@@ -41,7 +41,6 @@ def sample_pending_review(test_user_id: str) -> PendingHumanReviewModel:
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_123",
payload={"data": "test payload", "value": 42},
instructions="Please review this data",
editable=True,
@@ -161,7 +160,6 @@ def test_process_review_action_approve_success(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_123",
payload={"data": "modified payload", "value": 50},
instructions="Please review this data",
editable=True,
@@ -225,7 +223,6 @@ def test_process_review_action_reject_success(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_123",
payload={"data": "test payload"},
instructions="Please review",
editable=True,
@@ -277,7 +274,6 @@ def test_process_review_action_mixed_success(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_456",
payload={"data": "second payload"},
instructions="Second review",
editable=False,
@@ -307,7 +303,6 @@ def test_process_review_action_mixed_success(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_123",
payload={"data": "modified"},
instructions="Please review",
editable=True,
@@ -326,7 +321,6 @@ def test_process_review_action_mixed_success(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_456",
payload={"data": "second payload"},
instructions="Second review",
editable=False,

View File

@@ -5,7 +5,7 @@ import autogpt_libs.auth as autogpt_auth_lib
from fastapi import APIRouter, HTTPException, Query, Security, status
from prisma.enums import ReviewStatus
from backend.data.execution import ExecutionContext, get_graph_execution_meta
from backend.data.execution import get_graph_execution_meta
from backend.data.human_review import (
get_pending_reviews_for_execution,
get_pending_reviews_for_user,
@@ -169,23 +169,10 @@ async def process_review_action(
if not still_has_pending:
# Resume execution
try:
# If auto_approve_node_ids is set, create a context that will
# automatically approve future reviews from these specific nodes
execution_context = None
if request.auto_approve_node_ids:
execution_context = ExecutionContext(
auto_approved_node_ids=set(request.auto_approve_node_ids),
)
logger.info(
f"Auto-approving future reviews for nodes "
f"{request.auto_approve_node_ids} in execution {graph_exec_id}"
)
await add_graph_execution(
graph_id=first_review.graph_id,
user_id=user_id,
graph_exec_id=graph_exec_id,
execution_context=execution_context,
)
logger.info(f"Resumed execution {graph_exec_id}")
except Exception as e:

View File

@@ -154,16 +154,16 @@ async def store_content_embedding(
# Upsert the embedding
# WHERE clause in DO UPDATE prevents PostgreSQL 15 bug with NULLS NOT DISTINCT
# Use {pgvector_schema}.vector for explicit pgvector type qualification
# Use unqualified ::vector - pgvector is in search_path on all environments
await execute_raw_with_schema(
"""
INSERT INTO {schema_prefix}"UnifiedContentEmbedding" (
"id", "contentType", "contentId", "userId", "embedding", "searchableText", "metadata", "createdAt", "updatedAt"
)
VALUES (gen_random_uuid()::text, $1::{schema_prefix}"ContentType", $2, $3, $4::{pgvector_schema}.vector, $5, $6::jsonb, NOW(), NOW())
VALUES (gen_random_uuid()::text, $1::{schema_prefix}"ContentType", $2, $3, $4::vector, $5, $6::jsonb, NOW(), NOW())
ON CONFLICT ("contentType", "contentId", "userId")
DO UPDATE SET
"embedding" = $4::{pgvector_schema}.vector,
"embedding" = $4::vector,
"searchableText" = $5,
"metadata" = $6::jsonb,
"updatedAt" = NOW()
@@ -879,8 +879,7 @@ async def semantic_search(
min_similarity_idx = len(params) + 1
params.append(min_similarity)
# Use regular string (not f-string) for template to preserve {schema_prefix} and {schema} placeholders
# Use OPERATOR({pgvector_schema}.<=>) for explicit operator schema qualification
# Use unqualified ::vector and <=> operator - pgvector is in search_path on all environments
sql = (
"""
SELECT
@@ -888,9 +887,9 @@ async def semantic_search(
"contentType" as content_type,
"searchableText" as searchable_text,
metadata,
1 - (embedding OPERATOR({pgvector_schema}.<=>) '"""
1 - (embedding <=> '"""
+ embedding_str
+ """'::{pgvector_schema}.vector) as similarity
+ """'::vector) as similarity
FROM {schema_prefix}"UnifiedContentEmbedding"
WHERE "contentType" IN ("""
+ content_type_placeholders
@@ -898,9 +897,9 @@ async def semantic_search(
"""
+ user_filter
+ """
AND 1 - (embedding OPERATOR({pgvector_schema}.<=>) '"""
AND 1 - (embedding <=> '"""
+ embedding_str
+ """'::{pgvector_schema}.vector) >= $"""
+ """'::vector) >= $"""
+ str(min_similarity_idx)
+ """
ORDER BY similarity DESC

View File

@@ -295,7 +295,7 @@ async def unified_hybrid_search(
FROM {{schema_prefix}}"UnifiedContentEmbedding" uce
WHERE uce."contentType" = ANY({content_types_param}::{{schema_prefix}}"ContentType"[])
{user_filter}
ORDER BY uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector
ORDER BY uce.embedding <=> {embedding_param}::vector
LIMIT 200
)
),
@@ -307,7 +307,7 @@ async def unified_hybrid_search(
uce.metadata,
uce."updatedAt" as updated_at,
-- Semantic score: cosine similarity (1 - distance)
COALESCE(1 - (uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector), 0) as semantic_score,
COALESCE(1 - (uce.embedding <=> {embedding_param}::vector), 0) as semantic_score,
-- Lexical score: ts_rank_cd
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
-- Category match from metadata
@@ -583,7 +583,7 @@ async def hybrid_search(
WHERE uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
AND uce."userId" IS NULL
AND {where_clause}
ORDER BY uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector
ORDER BY uce.embedding <=> {embedding_param}::vector
LIMIT 200
) uce
),
@@ -605,7 +605,7 @@ async def hybrid_search(
-- Searchable text for BM25 reranking
COALESCE(sa.agent_name, '') || ' ' || COALESCE(sa.sub_heading, '') || ' ' || COALESCE(sa.description, '') as searchable_text,
-- Semantic score
COALESCE(1 - (uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector), 0) as semantic_score,
COALESCE(1 - (uce.embedding <=> {embedding_param}::vector), 0) as semantic_score,
-- Lexical score (raw, will normalize)
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
-- Category match

View File

@@ -55,7 +55,6 @@ class HITLReviewHelper:
async def _handle_review_request(
input_data: Any,
user_id: str,
node_id: str,
node_exec_id: str,
graph_exec_id: str,
graph_id: str,
@@ -63,7 +62,6 @@ class HITLReviewHelper:
execution_context: ExecutionContext,
block_name: str = "Block",
editable: bool = False,
skip_safe_mode_check: bool = False,
) -> Optional[ReviewResult]:
"""
Handle a review request for a block that requires human review.
@@ -71,7 +69,6 @@ class HITLReviewHelper:
Args:
input_data: The input data to be reviewed
user_id: ID of the user requesting the review
node_id: ID of the node in the graph definition
node_exec_id: ID of the node execution
graph_exec_id: ID of the graph execution
graph_id: ID of the graph
@@ -79,8 +76,6 @@ class HITLReviewHelper:
execution_context: Current execution context
block_name: Name of the block requesting review
editable: Whether the reviewer can edit the data
skip_safe_mode_check: If True, skip the safe mode check (caller already
verified). Used by sensitive action blocks that check their own flag.
Returns:
ReviewResult if review is complete, None if waiting for human input
@@ -89,11 +84,7 @@ class HITLReviewHelper:
Exception: If review creation or status update fails
"""
# Skip review if safe mode is disabled - return auto-approved result
# (unless caller already checked and wants to skip this check)
if (
not skip_safe_mode_check
and not execution_context.human_in_the_loop_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"
)
@@ -105,27 +96,12 @@ class HITLReviewHelper:
node_exec_id=node_exec_id,
)
# Skip review if this specific node has been auto-approved by the user
if node_id in execution_context.auto_approved_node_ids:
logger.info(
f"Block {block_name} skipping review for node {node_exec_id} - "
f"node {node_id} is auto-approved"
)
return ReviewResult(
data=input_data,
status=ReviewStatus.APPROVED,
message="Auto-approved (user approved all future actions for this block)",
processed=True,
node_exec_id=node_exec_id,
)
result = await HITLReviewHelper.get_or_create_human_review(
user_id=user_id,
node_exec_id=node_exec_id,
graph_exec_id=graph_exec_id,
graph_id=graph_id,
graph_version=graph_version,
node_id=node_id,
input_data=input_data,
message=f"Review required for {block_name} execution",
editable=editable,
@@ -153,7 +129,6 @@ class HITLReviewHelper:
async def handle_review_decision(
input_data: Any,
user_id: str,
node_id: str,
node_exec_id: str,
graph_exec_id: str,
graph_id: str,
@@ -161,7 +136,6 @@ class HITLReviewHelper:
execution_context: ExecutionContext,
block_name: str = "Block",
editable: bool = False,
skip_safe_mode_check: bool = False,
) -> Optional[ReviewDecision]:
"""
Handle a review request and return the decision in a single call.
@@ -169,7 +143,6 @@ class HITLReviewHelper:
Args:
input_data: The input data to be reviewed
user_id: ID of the user requesting the review
node_id: ID of the node in the graph definition
node_exec_id: ID of the node execution
graph_exec_id: ID of the graph execution
graph_id: ID of the graph
@@ -177,8 +150,6 @@ class HITLReviewHelper:
execution_context: Current execution context
block_name: Name of the block requesting review
editable: Whether the reviewer can edit the data
skip_safe_mode_check: If True, skip the safe mode check (caller already
verified). Used by sensitive action blocks that check their own flag.
Returns:
ReviewDecision if review is complete (approved/rejected),
@@ -187,7 +158,6 @@ class HITLReviewHelper:
review_result = await HITLReviewHelper._handle_review_request(
input_data=input_data,
user_id=user_id,
node_id=node_id,
node_exec_id=node_exec_id,
graph_exec_id=graph_exec_id,
graph_id=graph_id,
@@ -195,7 +165,6 @@ class HITLReviewHelper:
execution_context=execution_context,
block_name=block_name,
editable=editable,
skip_safe_mode_check=skip_safe_mode_check,
)
if review_result is None:

View File

@@ -97,7 +97,6 @@ class HumanInTheLoopBlock(Block):
input_data: Input,
*,
user_id: str,
node_id: str,
node_exec_id: str,
graph_exec_id: str,
graph_id: str,
@@ -105,16 +104,6 @@ class HumanInTheLoopBlock(Block):
execution_context: ExecutionContext,
**_kwargs,
) -> BlockOutput:
# Check if this specific node has been auto-approved by the user
if node_id in execution_context.auto_approved_node_ids:
logger.info(
f"HITL block skipping review for node {node_exec_id} - "
f"node {node_id} is auto-approved"
)
yield "approved_data", input_data.data
yield "review_message", "Auto-approved (user approved all future actions for this block)"
return
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"
@@ -126,7 +115,6 @@ class HumanInTheLoopBlock(Block):
decision = await self.handle_review_decision(
input_data=input_data.data,
user_id=user_id,
node_id=node_id,
node_exec_id=node_exec_id,
graph_exec_id=graph_exec_id,
graph_id=graph_id,

View File

@@ -622,7 +622,6 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
input_data: BlockInput,
*,
user_id: str,
node_id: str,
node_exec_id: str,
graph_exec_id: str,
graph_id: str,
@@ -649,7 +648,6 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
decision = await HITLReviewHelper.handle_review_decision(
input_data=input_data,
user_id=user_id,
node_id=node_id,
node_exec_id=node_exec_id,
graph_exec_id=graph_exec_id,
graph_id=graph_id,

View File

@@ -121,10 +121,14 @@ async def _raw_with_schema(
Supports placeholders:
- {schema_prefix}: Table/type prefix (e.g., "platform".)
- {schema}: Raw schema name for application tables (e.g., platform)
- {pgvector_schema}: Schema where pgvector is installed (defaults to "public")
Note on pgvector types:
Use unqualified ::vector and <=> operator in queries. PostgreSQL resolves
these via search_path, which includes the schema where pgvector is installed
on all environments (local, CI, dev).
Args:
query_template: SQL query with {schema_prefix}, {schema}, and/or {pgvector_schema} placeholders
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
*args: Query parameters
execute: If False, executes SELECT query. If True, executes INSERT/UPDATE/DELETE.
client: Optional Prisma client for transactions (only used when execute=True).
@@ -135,20 +139,16 @@ async def _raw_with_schema(
Example with vector type:
await execute_raw_with_schema(
'INSERT INTO {schema_prefix}"Embedding" (vec) VALUES ($1::{pgvector_schema}.vector)',
'INSERT INTO {schema_prefix}"Embedding" (vec) VALUES ($1::vector)',
embedding_data
)
"""
schema = get_database_schema()
schema_prefix = f'"{schema}".' if schema != "public" else ""
# pgvector extension is typically installed in "public" schema
# On Supabase it may be in "extensions" but "public" is the common default
pgvector_schema = "public"
formatted_query = query_template.format(
schema_prefix=schema_prefix,
schema=schema,
pgvector_schema=pgvector_schema,
)
import prisma as prisma_module

View File

@@ -103,8 +103,18 @@ class RedisEventBus(BaseRedisEventBus[M], ABC):
return redis.get_redis()
def publish_event(self, event: M, channel_key: str):
message, full_channel_name = self._serialize_message(event, channel_key)
self.connection.publish(full_channel_name, message)
"""
Publish an event to Redis. Gracefully handles connection failures
by logging the error instead of raising exceptions.
"""
try:
message, full_channel_name = self._serialize_message(event, channel_key)
self.connection.publish(full_channel_name, message)
except Exception:
logger.exception(
f"Failed to publish event to Redis channel {channel_key}. "
"Event bus operation will continue without Redis connectivity."
)
def listen_events(self, channel_key: str) -> Generator[M, None, None]:
pubsub, full_channel_name = self._get_pubsub_channel(
@@ -128,9 +138,19 @@ class AsyncRedisEventBus(BaseRedisEventBus[M], ABC):
return await redis.get_redis_async()
async def publish_event(self, event: M, channel_key: str):
message, full_channel_name = self._serialize_message(event, channel_key)
connection = await self.connection
await connection.publish(full_channel_name, message)
"""
Publish an event to Redis. Gracefully handles connection failures
by logging the error instead of raising exceptions.
"""
try:
message, full_channel_name = self._serialize_message(event, channel_key)
connection = await self.connection
await connection.publish(full_channel_name, message)
except Exception:
logger.exception(
f"Failed to publish event to Redis channel {channel_key}. "
"Event bus operation will continue without Redis connectivity."
)
async def listen_events(self, channel_key: str) -> AsyncGenerator[M, None]:
pubsub, full_channel_name = self._get_pubsub_channel(

View File

@@ -0,0 +1,56 @@
"""
Tests for event_bus graceful degradation when Redis is unavailable.
"""
from unittest.mock import AsyncMock, patch
import pytest
from pydantic import BaseModel
from backend.data.event_bus import AsyncRedisEventBus
class TestEvent(BaseModel):
"""Test event model."""
message: str
class TestNotificationBus(AsyncRedisEventBus[TestEvent]):
"""Test implementation of AsyncRedisEventBus."""
Model = TestEvent
@property
def event_bus_name(self) -> str:
return "test_event_bus"
@pytest.mark.asyncio
async def test_publish_event_handles_connection_failure_gracefully():
"""Test that publish_event logs exception instead of raising when Redis is unavailable."""
bus = TestNotificationBus()
event = TestEvent(message="test message")
# Mock get_redis_async to raise connection error
with patch(
"backend.data.event_bus.redis.get_redis_async",
side_effect=ConnectionError("Authentication required."),
):
# Should not raise exception
await bus.publish_event(event, "test_channel")
@pytest.mark.asyncio
async def test_publish_event_works_with_redis_available():
"""Test that publish_event works normally when Redis is available."""
bus = TestNotificationBus()
event = TestEvent(message="test message")
# Mock successful Redis connection
mock_redis = AsyncMock()
mock_redis.publish = AsyncMock()
with patch("backend.data.event_bus.redis.get_redis_async", return_value=mock_redis):
await bus.publish_event(event, "test_channel")
mock_redis.publish.assert_called_once()

View File

@@ -81,12 +81,13 @@ class ExecutionContext(BaseModel):
This includes information needed by blocks, sub-graphs, and execution management.
"""
model_config = {"extra": "ignore"}
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
auto_approved_node_ids: set[str] = Field(default_factory=set)
# -------------------------- Models -------------------------- #

View File

@@ -64,6 +64,8 @@ logger = logging.getLogger(__name__)
class GraphSettings(BaseModel):
# Use Annotated with BeforeValidator to coerce None to default values.
# This handles cases where the database has null values for these fields.
model_config = {"extra": "ignore"}
human_in_the_loop_safe_mode: Annotated[
bool, BeforeValidator(lambda v: v if v is not None else True)
] = True

View File

@@ -38,7 +38,6 @@ async def get_or_create_human_review(
graph_exec_id: str,
graph_id: str,
graph_version: int,
node_id: str,
input_data: SafeJsonData,
message: str,
editable: bool,
@@ -54,7 +53,6 @@ async def get_or_create_human_review(
graph_exec_id: ID of the graph execution
graph_id: ID of the graph template
graph_version: Version of the graph template
node_id: ID of the node in the graph definition
input_data: The data to be reviewed
message: Instructions for the reviewer
editable: Whether the data can be edited
@@ -75,7 +73,6 @@ async def get_or_create_human_review(
"graphExecId": graph_exec_id,
"graphId": graph_id,
"graphVersion": graph_version,
"nodeId": node_id,
"payload": SafeJson(input_data),
"instructions": message,
"editable": editable,

View File

@@ -23,7 +23,6 @@ def sample_db_review():
mock_review.graphExecId = "test_graph_exec_456"
mock_review.graphId = "test_graph_789"
mock_review.graphVersion = 1
mock_review.nodeId = "node_def_123"
mock_review.payload = {"data": "test payload"}
mock_review.instructions = "Please review"
mock_review.editable = True
@@ -56,7 +55,6 @@ async def test_get_or_create_human_review_new(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_123",
input_data={"data": "test payload"},
message="Please review",
editable=True,
@@ -86,7 +84,6 @@ async def test_get_or_create_human_review_approved(
graph_exec_id="test_graph_exec_456",
graph_id="test_graph_789",
graph_version=1,
node_id="node_def_123",
input_data={"data": "test payload"},
message="Please review",
editable=True,
@@ -186,7 +183,6 @@ async def test_process_all_reviews_for_execution_success(
updated_review.graphExecId = "test_graph_exec_456"
updated_review.graphId = "test_graph_789"
updated_review.graphVersion = 1
updated_review.nodeId = "node_def_123"
updated_review.payload = {"data": "modified"}
updated_review.instructions = "Please review"
updated_review.editable = True
@@ -276,7 +272,6 @@ async def test_process_all_reviews_mixed_approval_rejection(
second_review.graphExecId = "test_graph_exec_456"
second_review.graphId = "test_graph_789"
second_review.graphVersion = 1
second_review.nodeId = "node_def_456"
second_review.payload = {"data": "original"}
second_review.instructions = "Second review"
second_review.editable = True
@@ -301,7 +296,6 @@ async def test_process_all_reviews_mixed_approval_rejection(
approved_review.graphExecId = "test_graph_exec_456"
approved_review.graphId = "test_graph_789"
approved_review.graphVersion = 1
approved_review.nodeId = "node_def_123"
approved_review.payload = {"data": "modified"}
approved_review.instructions = "Please review"
approved_review.editable = True
@@ -319,7 +313,6 @@ async def test_process_all_reviews_mixed_approval_rejection(
rejected_review.graphExecId = "test_graph_exec_456"
rejected_review.graphId = "test_graph_789"
rejected_review.graphVersion = 1
rejected_review.nodeId = "node_def_456"
rejected_review.payload = {"data": "original"}
rejected_review.instructions = "Please review"
rejected_review.editable = True

View File

@@ -1,9 +1,10 @@
-- CreateExtension
-- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first
-- Create in public schema so vector type is available across all schemas
-- Creates extension in current schema (determined by search_path from DATABASE_URL ?schema= param)
-- This ensures vector type is in the same schema as tables, making ::vector work without explicit qualification
DO $$
BEGIN
CREATE EXTENSION IF NOT EXISTS "vector" WITH SCHEMA "public";
CREATE EXTENSION IF NOT EXISTS "vector";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'vector extension not available or already exists, skipping';
END $$;
@@ -19,7 +20,7 @@ CREATE TABLE "UnifiedContentEmbedding" (
"contentType" "ContentType" NOT NULL,
"contentId" TEXT NOT NULL,
"userId" TEXT,
"embedding" public.vector(1536) NOT NULL,
"embedding" vector(1536) NOT NULL,
"searchableText" TEXT NOT NULL,
"metadata" JSONB NOT NULL DEFAULT '{}',
@@ -45,4 +46,4 @@ CREATE UNIQUE INDEX "UnifiedContentEmbedding_contentType_contentId_userId_key" O
-- Uses cosine distance operator (<=>), which matches the query in hybrid_search.py
-- Note: Drop first in case Prisma created a btree index (Prisma doesn't support HNSW)
DROP INDEX IF EXISTS "UnifiedContentEmbedding_embedding_idx";
CREATE INDEX "UnifiedContentEmbedding_embedding_idx" ON "UnifiedContentEmbedding" USING hnsw ("embedding" public.vector_cosine_ops);
CREATE INDEX "UnifiedContentEmbedding_embedding_idx" ON "UnifiedContentEmbedding" USING hnsw ("embedding" vector_cosine_ops);

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "PendingHumanReview" ADD COLUMN "nodeId" TEXT NOT NULL DEFAULT '';

View File

@@ -573,7 +573,6 @@ model PendingHumanReview {
graphExecId String
graphId String
graphVersion Int
nodeId String // The node ID in the graph definition (for auto-approval tracking)
payload Json // The actual payload data to be reviewed
instructions String? // Instructions/message for the reviewer
editable Boolean @default(true) // Whether the reviewer can edit the data

View File

@@ -86,6 +86,7 @@ export function FloatingSafeModeToggle({
const {
currentHITLSafeMode,
showHITLToggle,
isHITLStateUndetermined,
handleHITLToggle,
currentSensitiveActionSafeMode,
showSensitiveActionToggle,
@@ -98,9 +99,16 @@ export function FloatingSafeModeToggle({
return null;
}
const showHITL = showHITLToggle && !isHITLStateUndetermined;
const showSensitive = showSensitiveActionToggle;
if (!showHITL && !showSensitive) {
return null;
}
return (
<div className={cn("fixed z-50 flex flex-col gap-2", className)}>
{showHITLToggle && (
{showHITL && (
<SafeModeButton
isEnabled={currentHITLSafeMode}
label="Human in the loop block approval"
@@ -111,7 +119,7 @@ export function FloatingSafeModeToggle({
fullWidth={fullWidth}
/>
)}
{showSensitiveActionToggle && (
{showSensitive && (
<SafeModeButton
isEnabled={currentSensitiveActionSafeMode}
label="Sensitive actions blocks approval"

View File

@@ -14,10 +14,6 @@ import {
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useEffect, useRef, useState } from "react";
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
import {
AIAgentSafetyPopup,
useAIAgentSafetyPopup,
} from "./components/AIAgentSafetyPopup/AIAgentSafetyPopup";
import { ModalHeader } from "./components/ModalHeader/ModalHeader";
import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection";
import { RunActions } from "./components/RunActions/RunActions";
@@ -87,17 +83,8 @@ export function RunAgentModal({
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
const [hasOverflow, setHasOverflow] = useState(false);
const [isSafetyPopupOpen, setIsSafetyPopupOpen] = useState(false);
const [pendingRunAction, setPendingRunAction] = useState<(() => void) | null>(
null,
);
const contentRef = useRef<HTMLDivElement>(null);
const { shouldShowPopup, dismissPopup } = useAIAgentSafetyPopup(
agent.has_sensitive_action,
agent.has_human_in_the_loop,
);
const hasAnySetupFields =
Object.keys(agentInputFields || {}).length > 0 ||
Object.keys(agentCredentialsInputFields || {}).length > 0;
@@ -178,24 +165,6 @@ export function RunAgentModal({
onScheduleCreated?.(schedule);
}
function handleRunWithSafetyCheck() {
if (shouldShowPopup) {
setPendingRunAction(() => handleRun);
setIsSafetyPopupOpen(true);
} else {
handleRun();
}
}
function handleSafetyPopupAcknowledge() {
setIsSafetyPopupOpen(false);
dismissPopup();
if (pendingRunAction) {
pendingRunAction();
setPendingRunAction(null);
}
}
return (
<>
<Dialog
@@ -279,7 +248,7 @@ export function RunAgentModal({
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRunWithSafetyCheck}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
@@ -297,11 +266,6 @@ export function RunAgentModal({
</div>
</Dialog.Content>
</Dialog>
<AIAgentSafetyPopup
isOpen={isSafetyPopupOpen}
onAcknowledge={handleSafetyPopupAcknowledge}
/>
</>
);
}

View File

@@ -1,95 +0,0 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Key, storage } from "@/services/storage/local-storage";
import { ShieldCheckIcon } from "@phosphor-icons/react";
import { useCallback, useEffect, useState } from "react";
interface Props {
onAcknowledge: () => void;
isOpen: boolean;
}
export function AIAgentSafetyPopup({ onAcknowledge, isOpen }: Props) {
function handleAcknowledge() {
// Mark popup as shown so it won't appear again
storage.set(Key.AI_AGENT_SAFETY_POPUP_SHOWN, "true");
onAcknowledge();
}
if (!isOpen) return null;
return (
<Dialog
controlled={{ isOpen, set: () => {} }}
styling={{ maxWidth: "480px" }}
>
<Dialog.Content>
<div className="flex flex-col items-center p-6 text-center">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-50">
<ShieldCheckIcon
weight="fill"
size={32}
className="text-blue-600"
/>
</div>
<Text variant="h3" className="mb-4">
Safety Checks Enabled
</Text>
<Text variant="body" className="mb-2 text-zinc-700">
AI-generated agents may take actions that affect your data or
external systems.
</Text>
<Text variant="body" className="mb-8 text-zinc-700">
AutoGPT includes safety checks so you&apos;ll always have the
opportunity to review and approve sensitive actions before they
happen.
</Text>
<Button
variant="primary"
size="large"
className="w-full"
onClick={handleAcknowledge}
>
Got it
</Button>
</div>
</Dialog.Content>
</Dialog>
);
}
export function useAIAgentSafetyPopup(
hasSensitiveAction: boolean,
hasHumanInTheLoop: boolean,
) {
const [shouldShowPopup, setShouldShowPopup] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
useEffect(() => {
// Only check once after mount (to avoid SSR issues)
if (hasChecked) return;
const hasSeenPopup =
storage.get(Key.AI_AGENT_SAFETY_POPUP_SHOWN) === "true";
const isRelevantAgent = hasSensitiveAction || hasHumanInTheLoop;
setShouldShowPopup(!hasSeenPopup && isRelevantAgent);
setHasChecked(true);
}, [hasSensitiveAction, hasHumanInTheLoop, hasChecked]);
const dismissPopup = useCallback(() => {
setShouldShowPopup(false);
}, []);
return {
shouldShowPopup,
dismissPopup,
};
}

View File

@@ -69,6 +69,7 @@ export function SafeModeToggle({ graph, className }: Props) {
const {
currentHITLSafeMode,
showHITLToggle,
isHITLStateUndetermined,
handleHITLToggle,
currentSensitiveActionSafeMode,
showSensitiveActionToggle,
@@ -77,13 +78,20 @@ export function SafeModeToggle({ graph, className }: Props) {
shouldShowToggle,
} = useAgentSafeMode(graph);
if (!shouldShowToggle) {
if (!shouldShowToggle || isHITLStateUndetermined) {
return null;
}
const showHITL = showHITLToggle && !isHITLStateUndetermined;
const showSensitive = showSensitiveActionToggle;
if (!showHITL && !showSensitive) {
return null;
}
return (
<div className={cn("flex gap-1", className)}>
{showHITLToggle && (
{showHITL && (
<SafeModeIconButton
isEnabled={currentHITLSafeMode}
label="Human-in-the-loop"
@@ -93,7 +101,7 @@ export function SafeModeToggle({ graph, className }: Props) {
isPending={isPending}
/>
)}
{showSensitiveActionToggle && (
{showSensitive && (
<SafeModeIconButton
isEnabled={currentSensitiveActionSafeMode}
label="Sensitive actions"

View File

@@ -1,8 +1,15 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
CredentialsMetaInput,
CredentialsType,
GraphExecutionID,
GraphMeta,
LibraryAgentPreset,
@@ -29,7 +36,11 @@ import {
} from "@/components/__legacy__/ui/icons";
import { Input } from "@/components/__legacy__/ui/input";
import { Button } from "@/components/atoms/Button/Button";
import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import {
findSavedCredentialByProviderAndType,
findSavedUserCredentialByProviderAndType,
} from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
useToast,
@@ -37,6 +48,7 @@ import {
} from "@/components/molecules/Toast/use-toast";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { cn, isEmpty } from "@/lib/utils";
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react";
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
@@ -90,6 +102,7 @@ export function AgentRunDraftView({
const api = useBackendAPI();
const { toast } = useToast();
const toastOnFail = useToastOnFail();
const allProviders = useContext(CredentialsProvidersContext);
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const [inputCredentials, setInputCredentials] = useState<
@@ -128,6 +141,77 @@ export function AgentRunDraftView({
() => graph.credentials_input_schema.properties,
[graph],
);
const credentialFields = useMemo(
function getCredentialFields() {
return Object.entries(agentCredentialsInputFields);
},
[agentCredentialsInputFields],
);
const requiredCredentials = useMemo(
function getRequiredCredentials() {
return new Set(
(graph.credentials_input_schema?.required as string[]) || [],
);
},
[graph.credentials_input_schema?.required],
);
useEffect(
function initializeDefaultCredentials() {
if (!allProviders) return;
if (!graph.credentials_input_schema?.properties) return;
if (requiredCredentials.size === 0) return;
setInputCredentials(function updateCredentials(currentCreds) {
const next = { ...currentCreds };
let didAdd = false;
for (const key of requiredCredentials) {
if (next[key]) continue;
const schema = graph.credentials_input_schema.properties[key];
if (!schema) continue;
const providerNames = schema.credentials_provider || [];
const credentialTypes = schema.credentials_types || [];
const requiredScopes = schema.credentials_scopes;
const userCredential = findSavedUserCredentialByProviderAndType(
providerNames,
credentialTypes,
requiredScopes,
allProviders,
);
const savedCredential =
userCredential ||
findSavedCredentialByProviderAndType(
providerNames,
credentialTypes,
requiredScopes,
allProviders,
);
if (!savedCredential) continue;
next[key] = {
id: savedCredential.id,
provider: savedCredential.provider,
type: savedCredential.type as CredentialsType,
title: savedCredential.title,
};
didAdd = true;
}
if (!didAdd) return currentCreds;
return next;
});
},
[
allProviders,
graph.credentials_input_schema?.properties,
requiredCredentials,
],
);
const [allRequiredInputsAreSet, missingInputs] = useMemo(() => {
const nonEmptyInputs = new Set(
@@ -145,18 +229,35 @@ export function AgentRunDraftView({
);
return [isSuperset, difference];
}, [agentInputSchema.required, inputValues]);
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
const availableCredentials = new Set(Object.keys(inputCredentials));
const allCredentials = new Set(Object.keys(agentCredentialsInputFields));
// Backwards-compatible implementation of isSupersetOf and difference
const isSuperset = Array.from(allCredentials).every((item) =>
availableCredentials.has(item),
);
const difference = Array.from(allCredentials).filter(
(item) => !availableCredentials.has(item),
);
return [isSuperset, difference];
}, [agentCredentialsInputFields, inputCredentials]);
const [allCredentialsAreSet, missingCredentials] = useMemo(
function getCredentialStatus() {
const missing = Array.from(requiredCredentials).filter((key) => {
const cred = inputCredentials[key];
return !cred || !cred.id;
});
return [missing.length === 0, missing];
},
[requiredCredentials, inputCredentials],
);
function addChangedCredentials(prev: Set<keyof LibraryAgentPresetUpdatable>) {
const next = new Set(prev);
next.add("credentials");
return next;
}
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
setInputCredentials(function updateInputCredentials(currentCreds) {
const next = { ...currentCreds };
if (value === undefined) {
delete next[key];
return next;
}
next[key] = value;
return next;
});
setChangedPresetAttributes(addChangedCredentials);
}
const notifyMissingInputs = useCallback(
(needPresetName: boolean = true) => {
const allMissingFields = (
@@ -649,35 +750,6 @@ export function AgentRunDraftView({
</>
)}
{/* Credentials inputs */}
{Object.entries(agentCredentialsInputFields).map(
([key, inputSubSchema]) => (
<CredentialsInput
key={key}
schema={{ ...inputSubSchema, discriminator: undefined }}
selectedCredentials={
inputCredentials[key] ?? inputSubSchema.default
}
onSelectCredentials={(value) => {
setInputCredentials((obj) => {
const newObj = { ...obj };
if (value === undefined) {
delete newObj[key];
return newObj;
}
return {
...obj,
[key]: value,
};
});
setChangedPresetAttributes((prev) =>
prev.add("credentials"),
);
}}
/>
),
)}
{/* Regular inputs */}
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
<RunAgentInputs
@@ -695,6 +767,17 @@ export function AgentRunDraftView({
data-testid={`agent-input-${key}`}
/>
))}
{/* Credentials inputs */}
{credentialFields.length > 0 && (
<CredentialsGroupedView
credentialFields={credentialFields}
requiredCredentials={requiredCredentials}
inputCredentials={inputCredentials}
inputValues={inputValues}
onCredentialChange={handleCredentialChange}
/>
)}
</CardContent>
</Card>
</div>

View File

@@ -8829,11 +8829,6 @@
"title": "Graph Version",
"description": "Graph version"
},
"node_id": {
"type": "string",
"title": "Node Id",
"description": "Node ID in the graph definition"
},
"payload": {
"anyOf": [
{ "additionalProperties": true, "type": "object" },
@@ -8907,7 +8902,6 @@
"graph_exec_id",
"graph_id",
"graph_version",
"node_id",
"payload",
"editable",
"status",
@@ -9431,12 +9425,6 @@
"type": "array",
"title": "Reviews",
"description": "All reviews with their approval status, data, and messages"
},
"auto_approve_node_ids": {
"items": { "type": "string" },
"type": "array",
"title": "Auto Approve Node Ids",
"description": "List of node IDs (from the graph definition) to auto-approve for the remainder of this execution. Future reviews from these specific nodes will be automatically approved. This only affects the current execution run."
}
},
"type": "object",

View File

@@ -1,5 +1,5 @@
import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider";
import { getSystemCredentials } from "../../helpers";
import { filterSystemCredentials, getSystemCredentials } from "../../helpers";
export type CredentialField = [string, any];
@@ -208,3 +208,42 @@ export function findSavedCredentialByProviderAndType(
return undefined;
}
export function findSavedUserCredentialByProviderAndType(
providerNames: string[],
credentialTypes: string[],
requiredScopes: string[] | undefined,
allProviders: CredentialsProvidersContextType | null,
): SavedCredential | undefined {
for (const providerName of providerNames) {
const providerData = allProviders?.[providerName];
if (!providerData) continue;
const userCredentials = filterSystemCredentials(
providerData.savedCredentials ?? [],
);
const matchingCredentials: SavedCredential[] = [];
for (const credential of userCredentials) {
const typeMatches =
credentialTypes.length === 0 ||
credentialTypes.includes(credential.type);
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
if (!typeMatches) continue;
if (!scopesMatch) continue;
matchingCredentials.push(credential as SavedCredential);
}
if (matchingCredentials.length === 1) {
return matchingCredentials[0];
}
if (matchingCredentials.length > 1) {
return undefined;
}
}
return undefined;
}

View File

@@ -98,24 +98,20 @@ export function useCredentialsInput({
// Auto-select the first available credential on initial mount
// Once a user has made a selection, we don't override it
useEffect(() => {
if (readOnly) return;
if (!credentials || !("savedCredentials" in credentials)) return;
useEffect(
function autoSelectCredential() {
if (readOnly) return;
if (!credentials || !("savedCredentials" in credentials)) return;
if (selectedCredential?.id) return;
// If already selected, don't auto-select
if (selectedCredential?.id) return;
const savedCreds = credentials.savedCredentials;
if (savedCreds.length === 0) return;
// Only attempt auto-selection once
if (hasAttemptedAutoSelect.current) return;
hasAttemptedAutoSelect.current = true;
if (hasAttemptedAutoSelect.current) return;
hasAttemptedAutoSelect.current = true;
// If optional, don't auto-select (user can choose "None")
if (isOptional) return;
if (isOptional) return;
const savedCreds = credentials.savedCredentials;
// Auto-select the first credential if any are available
if (savedCreds.length > 0) {
const cred = savedCreds[0];
onSelectCredential({
id: cred.id,
@@ -123,14 +119,15 @@ export function useCredentialsInput({
provider: credentials.provider,
title: (cred as any).title,
});
}
}, [
credentials,
selectedCredential?.id,
readOnly,
isOptional,
onSelectCredential,
]);
},
[
credentials,
selectedCredential?.id,
readOnly,
isOptional,
onSelectCredential,
],
);
if (
!credentials ||

View File

@@ -37,7 +37,7 @@ export function PendingReviewsList({
>({});
const [pendingAction, setPendingAction] = useState<
"approve" | "approve-all" | "reject" | null
"approve" | "reject" | null
>(null);
const { toast } = useToast();
@@ -92,10 +92,7 @@ export function PendingReviewsList({
setReviewMessageMap((prev) => ({ ...prev, [nodeExecId]: message }));
}
function processReviews(
approved: boolean,
autoApproveFutureActions: boolean = false,
) {
function processReviews(approved: boolean) {
if (reviews.length === 0) {
toast({
title: "No reviews to process",
@@ -105,13 +102,7 @@ export function PendingReviewsList({
return;
}
setPendingAction(
autoApproveFutureActions
? "approve-all"
: approved
? "approve"
: "reject",
);
setPendingAction(approved ? "approve" : "reject");
const reviewItems = [];
for (const review of reviews) {
@@ -143,15 +134,9 @@ export function PendingReviewsList({
});
}
// Collect unique node_ids if auto-approving future actions
const autoApproveNodeIds = autoApproveFutureActions
? [...new Set(reviews.map((r) => r.node_id))]
: [];
reviewActionMutation.mutate({
data: {
reviews: reviewItems,
auto_approve_node_ids: autoApproveNodeIds,
},
});
}
@@ -206,8 +191,12 @@ export function PendingReviewsList({
))}
</div>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<div className="space-y-7">
<Text variant="body" className="text-textGrey">
Note: Changes you make here apply only to this task
</Text>
<div className="flex gap-2">
<Button
onClick={() => processReviews(true)}
disabled={reviewActionMutation.isPending || reviews.length === 0}
@@ -219,17 +208,6 @@ export function PendingReviewsList({
>
Approve
</Button>
<Button
onClick={() => processReviews(true, true)}
disabled={reviewActionMutation.isPending || reviews.length === 0}
variant="secondary"
className="flex items-center justify-center gap-2 rounded-full px-4 py-3"
loading={
pendingAction === "approve-all" && reviewActionMutation.isPending
}
>
Approve all future actions
</Button>
<Button
onClick={() => processReviews(false)}
disabled={reviewActionMutation.isPending || reviews.length === 0}
@@ -242,11 +220,6 @@ export function PendingReviewsList({
Reject
</Button>
</div>
<Text variant="small" className="text-textGrey">
You can turn auto-approval on or off anytime in this agent&apos;s
settings.
</Text>
</div>
</div>
);

View File

@@ -106,9 +106,14 @@ export function getTimezoneDisplayName(timezone: string): string {
const parts = timezone.split("/");
const city = parts[parts.length - 1].replace(/_/g, " ");
const abbr = getTimezoneAbbreviation(timezone);
return abbr ? `${city} (${abbr})` : city;
if (abbr && abbr !== timezone) {
return `${city} (${abbr})`;
}
// If abbreviation is same as timezone or not found, show timezone with underscores replaced
const timezoneDisplay = timezone.replace(/_/g, " ");
return `${city} (${timezoneDisplay})`;
} catch {
return timezone;
return timezone.replace(/_/g, " ");
}
}

View File

@@ -10,7 +10,6 @@ export enum Key {
LIBRARY_AGENTS_CACHE = "library-agents-cache",
CHAT_SESSION_ID = "chat_session_id",
COOKIE_CONSENT = "autogpt_cookie_consent",
AI_AGENT_SAFETY_POPUP_SHOWN = "ai-agent-safety-popup-shown",
}
function get(key: Key) {

View File

@@ -4,6 +4,7 @@ import { LoginPage } from "./pages/login.page";
import { MarketplacePage } from "./pages/marketplace.page";
import { hasMinCount, hasUrl, isVisible, matchesUrl } from "./utils/assertion";
// Marketplace tests for store agent search functionality
test.describe("Marketplace Basic Functionality", () => {
test("User can access marketplace page when logged out", async ({ page }) => {
const marketplacePage = new MarketplacePage(page);