Compare commits

...

6 Commits

Author SHA1 Message Date
Zamil Majdy
4eac081cf9 feat(frontend): update OpenAPI schema for is_ai_generated fields
- Add is_ai_generated field to CreateGraph model
- Add is_ai_generated_graph field to GraphSettings model
- Sync frontend API schema with backend changes
2026-01-12 16:30:34 -06:00
Zamil Majdy
eef7cf8cf8 fix(backend): preserve is_ai_generated_graph on fork and make parameter mandatory
- Fix bug where forking a library agent would reset is_ai_generated_graph to False
  - Now preserves the original agent's is_ai_generated_graph flag
  - Ensures forked AI-generated agents maintain safety requirements

- Make is_ai_generated parameter mandatory in create_library_agent()
  - Moved parameter before optional parameters for consistency
  - Removed default value to force explicit decisions at all call sites
  - Updated all 4 call sites to explicitly pass the parameter

- Update snapshot test for new is_ai_generated_graph field in settings

All 33 tests passing
2026-01-12 16:12:10 -06:00
Zamil Majdy
7c90b16209 Merge branch 'dev' into feat/ai-generated-mode 2026-01-12 15:41:14 -06:00
Zamil Majdy
3ec2f1953f refactor(backend): make is_ai_generated parameter mandatory in GraphSettings.from_graph()
- Removed default value from is_ai_generated parameter
- Forces all callers to explicitly specify whether graph is AI-generated
- Explicitly set is_ai_generated=False for store installations (curated user-published agents)
- Makes the code more explicit and prevents accidental defaults
2026-01-12 15:35:24 -06:00
Zamil Majdy
8d3dfe2cf8 fix(backend): preserve is_ai_generated_graph flag on version update
When updating a library agent to a new version, the is_ai_generated_graph
flag was being reset to False (the default), disabling human review for
AI-generated graphs on subsequent runs.

Fix: Pass the existing is_ai_generated_graph value from library.settings
to GraphSettings.from_graph() to preserve the flag across version updates.

Uses from_graph() to reduce duplication while preserving all settings.
2026-01-12 15:22:13 -06:00
Zamil Majdy
7133580d43 feat(backend): add is_ai_generated_graph setting and cleanup human_in_the_loop_safe_mode duplication
- Add is_ai_generated_graph field to GraphSettings with default false
- Add GraphSettings.from_graph() class method for initialization
- Add is_ai_generated_graph to ExecutionContext
- Update block review logic: required_human_review blocks only pause for review if safe_mode=True AND is_ai_generated_graph=True
- HITL blocks continue to respect only safe_mode (unchanged)
- Expose is_ai_generated in CreateGraph API model and create_new_graph endpoint

Cleanup:
- Remove redundant update_library_agent_settings wrapper function (25 lines)
- Inline human_in_the_loop_safe_mode initialization logic using GraphSettings.from_graph()
- Remove unnecessary branching and comments
- Total: 68 lines removed, 34 lines added

Tests: All 33 tests passing (26 API + 7 executor)
2026-01-12 15:12:41 -06:00
11 changed files with 62 additions and 73 deletions

View File

@@ -166,6 +166,7 @@ async def get_or_create_library_agent(
library_agents = await library_db.create_library_agent(
graph=graph,
user_id=user_id,
is_ai_generated=False,
create_library_agents_for_sub_graphs=False,
)
assert len(library_agents) == 1, "Expected 1 library agent to be created"

View File

@@ -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,
is_ai_generated: bool,
create_library_agents_for_sub_graphs: bool = True,
) -> list[library_model.LibraryAgent]:
"""
@@ -431,6 +414,7 @@ async def create_library_agent(
agent: The agent/Graph to add to the library.
user_id: The user to whom the agent will be added.
create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well.
is_ai_generated: Whether this graph was AI-generated.
Returns:
The newly created LibraryAgent records.
@@ -465,7 +449,9 @@ async def create_library_agent(
}
},
settings=SafeJson(
_initialize_graph_settings(graph_entry).model_dump()
GraphSettings.from_graph(
graph_entry, is_ai_generated=is_ai_generated
).model_dump()
),
),
include=library_agent_include(
@@ -627,33 +613,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 +797,9 @@ 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, is_ai_generated=False
).model_dump()
),
},
include=library_agent_include(
@@ -1228,8 +1189,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 is_ai_generated flag
return (
await create_library_agent(
new_graph,
user_id,
is_ai_generated=original_agent.settings.is_ai_generated_graph,
)
)[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

View File

@@ -762,10 +762,10 @@ 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, is_ai_generated=create_graph.is_ai_generated
)
activated_graph = await on_graph_activate(graph, user_id=user_id)
if create_graph.source == "builder":
@@ -889,21 +889,17 @@ 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(
agent_graph, is_ai_generated=library.settings.is_ai_generated_graph
)
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
@@ -920,21 +916,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)

View File

@@ -43,6 +43,7 @@ GraphExecutionSource = Literal["builder", "library", "onboarding"]
class CreateGraph(pydantic.BaseModel):
graph: Graph
source: GraphCreationSource | None = None
is_ai_generated: bool = False
class CreateAPIKeyRequest(pydantic.BaseModel):

View File

@@ -637,8 +637,11 @@ 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.requires_human_review
and execution_context.safe_mode
and execution_context.is_ai_generated_graph
):
return False, input_data
from backend.blocks.helpers.review import HITLReviewHelper

View File

@@ -82,6 +82,7 @@ class ExecutionContext(BaseModel):
"""
safe_mode: bool = True
is_ai_generated_graph: bool = False
user_timezone: str = "UTC"
root_execution_id: Optional[str] = None
parent_execution_id: Optional[str] = None

View File

@@ -63,6 +63,14 @@ logger = logging.getLogger(__name__)
class GraphSettings(BaseModel):
human_in_the_loop_safe_mode: bool | None = None
is_ai_generated_graph: bool = False
@classmethod
def from_graph(cls, graph: "GraphModel", is_ai_generated: bool) -> "GraphSettings":
return cls(
human_in_the_loop_safe_mode=(True if graph.has_human_in_the_loop else None),
is_ai_generated_graph=is_ai_generated,
)
class Link(BaseDbModel):

View File

@@ -877,6 +877,7 @@ async def add_graph_execution(
if settings.human_in_the_loop_safe_mode is not None
else True
),
is_ai_generated_graph=settings.is_ai_generated_graph,
user_timezone=(
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
),

View File

@@ -34,7 +34,8 @@
"is_favorite": false,
"recommended_schedule_cron": null,
"settings": {
"human_in_the_loop_safe_mode": null
"human_in_the_loop_safe_mode": null,
"is_ai_generated_graph": false
},
"marketplace_listing": null
},
@@ -72,7 +73,8 @@
"is_favorite": false,
"recommended_schedule_cron": null,
"settings": {
"human_in_the_loop_safe_mode": null
"human_in_the_loop_safe_mode": null,
"is_ai_generated_graph": false
},
"marketplace_listing": null
}

View File

@@ -412,7 +412,9 @@ class TestDataCreator:
# Use the API function to create library agent
library_agents.extend(
v.model_dump()
for v in await create_library_agent(graph, user["id"])
for v in await create_library_agent(
graph, user["id"], is_ai_generated=False
)
)
except Exception as e:
print(f"Error creating library agent: {e}")

View File

@@ -6613,6 +6613,11 @@
{ "type": "null" }
],
"title": "Source"
},
"is_ai_generated": {
"type": "boolean",
"title": "Is Ai Generated",
"default": false
}
},
"type": "object",
@@ -7558,6 +7563,11 @@
"human_in_the_loop_safe_mode": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Human In The Loop Safe Mode"
},
"is_ai_generated_graph": {
"type": "boolean",
"title": "Is Ai Generated Graph",
"default": false
}
},
"type": "object",