mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-05 12:25:04 -05:00
Compare commits
2 Commits
feat/text-
...
fix/sentry
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d58df37238 | ||
|
|
9c41512944 |
3
autogpt_platform/backend/.gitignore
vendored
3
autogpt_platform/backend/.gitignore
vendored
@@ -19,6 +19,3 @@ load-tests/*.json
|
|||||||
load-tests/*.log
|
load-tests/*.log
|
||||||
load-tests/node_modules/*
|
load-tests/node_modules/*
|
||||||
migrations/*/rollback*.sql
|
migrations/*/rollback*.sql
|
||||||
|
|
||||||
# Workspace files
|
|
||||||
workspaces/
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from backend.data.understanding import (
|
|||||||
get_business_understanding,
|
get_business_understanding,
|
||||||
)
|
)
|
||||||
from backend.util.exceptions import NotFoundError
|
from backend.util.exceptions import NotFoundError
|
||||||
from backend.util.settings import AppEnvironment, Settings
|
from backend.util.settings import Settings
|
||||||
|
|
||||||
from . import db as chat_db
|
from . import db as chat_db
|
||||||
from . import stream_registry
|
from . import stream_registry
|
||||||
@@ -222,18 +222,8 @@ async def _get_system_prompt_template(context: str) -> str:
|
|||||||
try:
|
try:
|
||||||
# cache_ttl_seconds=0 disables SDK caching to always get the latest prompt
|
# cache_ttl_seconds=0 disables SDK caching to always get the latest prompt
|
||||||
# Use asyncio.to_thread to avoid blocking the event loop
|
# Use asyncio.to_thread to avoid blocking the event loop
|
||||||
# In non-production environments, fetch the latest prompt version
|
|
||||||
# instead of the production-labeled version for easier testing
|
|
||||||
label = (
|
|
||||||
None
|
|
||||||
if settings.config.app_env == AppEnvironment.PRODUCTION
|
|
||||||
else "latest"
|
|
||||||
)
|
|
||||||
prompt = await asyncio.to_thread(
|
prompt = await asyncio.to_thread(
|
||||||
langfuse.get_prompt,
|
langfuse.get_prompt, config.langfuse_prompt_name, cache_ttl_seconds=0
|
||||||
config.langfuse_prompt_name,
|
|
||||||
label=label,
|
|
||||||
cache_ttl_seconds=0,
|
|
||||||
)
|
)
|
||||||
return prompt.compile(users_information=context)
|
return prompt.compile(users_information=context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -628,9 +618,6 @@ async def stream_chat_completion(
|
|||||||
total_tokens=chunk.totalTokens,
|
total_tokens=chunk.totalTokens,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif isinstance(chunk, StreamHeartbeat):
|
|
||||||
# Pass through heartbeat to keep SSE connection alive
|
|
||||||
yield chunk
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"Unknown chunk type: {type(chunk)}", exc_info=True)
|
logger.error(f"Unknown chunk type: {type(chunk)}", exc_info=True)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ from typing import Any, NotRequired, TypedDict
|
|||||||
|
|
||||||
from backend.api.features.library import db as library_db
|
from backend.api.features.library import db as library_db
|
||||||
from backend.api.features.store import db as store_db
|
from backend.api.features.store import db as store_db
|
||||||
from backend.data.graph import Graph, Link, Node, get_graph, get_store_listed_graphs
|
from backend.data.graph import (
|
||||||
|
Graph,
|
||||||
|
Link,
|
||||||
|
Node,
|
||||||
|
create_graph,
|
||||||
|
get_graph,
|
||||||
|
get_graph_all_versions,
|
||||||
|
get_store_listed_graphs,
|
||||||
|
)
|
||||||
from backend.util.exceptions import DatabaseError, NotFoundError
|
from backend.util.exceptions import DatabaseError, NotFoundError
|
||||||
|
|
||||||
from .service import (
|
from .service import (
|
||||||
@@ -20,6 +28,8 @@ from .service import (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
AGENT_EXECUTOR_BLOCK_ID = "e189baac-8c20-45a1-94a7-55177ea42565"
|
||||||
|
|
||||||
|
|
||||||
class ExecutionSummary(TypedDict):
|
class ExecutionSummary(TypedDict):
|
||||||
"""Summary of a single execution for quality assessment."""
|
"""Summary of a single execution for quality assessment."""
|
||||||
@@ -659,6 +669,45 @@ def json_to_graph(agent_json: dict[str, Any]) -> Graph:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _reassign_node_ids(graph: Graph) -> None:
|
||||||
|
"""Reassign all node and link IDs to new UUIDs.
|
||||||
|
|
||||||
|
This is needed when creating a new version to avoid unique constraint violations.
|
||||||
|
"""
|
||||||
|
id_map = {node.id: str(uuid.uuid4()) for node in graph.nodes}
|
||||||
|
|
||||||
|
for node in graph.nodes:
|
||||||
|
node.id = id_map[node.id]
|
||||||
|
|
||||||
|
for link in graph.links:
|
||||||
|
link.id = str(uuid.uuid4())
|
||||||
|
if link.source_id in id_map:
|
||||||
|
link.source_id = id_map[link.source_id]
|
||||||
|
if link.sink_id in id_map:
|
||||||
|
link.sink_id = id_map[link.sink_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_agent_executor_user_ids(agent_json: dict[str, Any], user_id: str) -> None:
|
||||||
|
"""Populate user_id in AgentExecutorBlock nodes.
|
||||||
|
|
||||||
|
The external agent generator creates AgentExecutorBlock nodes with empty user_id.
|
||||||
|
This function fills in the actual user_id so sub-agents run with correct permissions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_json: Agent JSON dict (modified in place)
|
||||||
|
user_id: User ID to set
|
||||||
|
"""
|
||||||
|
for node in agent_json.get("nodes", []):
|
||||||
|
if node.get("block_id") == AGENT_EXECUTOR_BLOCK_ID:
|
||||||
|
input_default = node.get("input_default") or {}
|
||||||
|
if not input_default.get("user_id"):
|
||||||
|
input_default["user_id"] = user_id
|
||||||
|
node["input_default"] = input_default
|
||||||
|
logger.debug(
|
||||||
|
f"Set user_id for AgentExecutorBlock node {node.get('id')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def save_agent_to_library(
|
async def save_agent_to_library(
|
||||||
agent_json: dict[str, Any], user_id: str, is_update: bool = False
|
agent_json: dict[str, Any], user_id: str, is_update: bool = False
|
||||||
) -> tuple[Graph, Any]:
|
) -> tuple[Graph, Any]:
|
||||||
@@ -672,10 +721,35 @@ async def save_agent_to_library(
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (created Graph, LibraryAgent)
|
Tuple of (created Graph, LibraryAgent)
|
||||||
"""
|
"""
|
||||||
|
# Populate user_id in AgentExecutorBlock nodes before conversion
|
||||||
|
_populate_agent_executor_user_ids(agent_json, user_id)
|
||||||
|
|
||||||
graph = json_to_graph(agent_json)
|
graph = json_to_graph(agent_json)
|
||||||
|
|
||||||
if is_update:
|
if is_update:
|
||||||
return await library_db.update_graph_in_library(graph, user_id)
|
if graph.id:
|
||||||
return await library_db.create_graph_in_library(graph, user_id)
|
existing_versions = await get_graph_all_versions(graph.id, user_id)
|
||||||
|
if existing_versions:
|
||||||
|
latest_version = max(v.version for v in existing_versions)
|
||||||
|
graph.version = latest_version + 1
|
||||||
|
_reassign_node_ids(graph)
|
||||||
|
logger.info(f"Updating agent {graph.id} to version {graph.version}")
|
||||||
|
else:
|
||||||
|
graph.id = str(uuid.uuid4())
|
||||||
|
graph.version = 1
|
||||||
|
_reassign_node_ids(graph)
|
||||||
|
logger.info(f"Creating new agent with ID {graph.id}")
|
||||||
|
|
||||||
|
created_graph = await create_graph(graph, user_id)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return created_graph, library_agents[0]
|
||||||
|
|
||||||
|
|
||||||
def graph_to_json(graph: Graph) -> dict[str, Any]:
|
def graph_to_json(graph: Graph) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -206,9 +206,9 @@ async def search_agents(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
no_results_msg = (
|
no_results_msg = (
|
||||||
f"No agents found matching '{query}'. Let the user know they can try different keywords or browse the marketplace. Also let them know you can create a custom agent for them based on their needs."
|
f"No agents found matching '{query}'. Try different keywords or browse the marketplace."
|
||||||
if source == "marketplace"
|
if source == "marketplace"
|
||||||
else f"No agents matching '{query}' found in your library. Let the user know you can create a custom agent for them based on their needs."
|
else f"No agents matching '{query}' found in your library."
|
||||||
)
|
)
|
||||||
return NoResultsResponse(
|
return NoResultsResponse(
|
||||||
message=no_results_msg, session_id=session_id, suggestions=suggestions
|
message=no_results_msg, session_id=session_id, suggestions=suggestions
|
||||||
@@ -224,10 +224,10 @@ async def search_agents(
|
|||||||
message = (
|
message = (
|
||||||
"Now you have found some options for the user to choose from. "
|
"Now you have found some options for the user to choose from. "
|
||||||
"You can add a link to a recommended agent at: /marketplace/agent/agent_id "
|
"You can add a link to a recommended agent at: /marketplace/agent/agent_id "
|
||||||
"Please ask the user if they would like to use any of these agents. Let the user know we can create a custom agent for them based on their needs."
|
"Please ask the user if they would like to use any of these agents."
|
||||||
if source == "marketplace"
|
if source == "marketplace"
|
||||||
else "Found agents in the user's library. You can provide a link to view an agent at: "
|
else "Found agents in the user's library. You can provide a link to view an agent at: "
|
||||||
"/library/agents/{agent_id}. Use agent_output to get execution results, or run_agent to execute. Let the user know we can create a custom agent for them based on their needs."
|
"/library/agents/{agent_id}. Use agent_output to get execution results, or run_agent to execute."
|
||||||
)
|
)
|
||||||
|
|
||||||
return AgentsFoundResponse(
|
return AgentsFoundResponse(
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ from backend.data.graph import GraphSettings
|
|||||||
from backend.data.includes import AGENT_PRESET_INCLUDE, library_agent_include
|
from backend.data.includes import AGENT_PRESET_INCLUDE, library_agent_include
|
||||||
from backend.data.model import CredentialsMetaInput
|
from backend.data.model import CredentialsMetaInput
|
||||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||||
from backend.integrations.webhooks.graph_lifecycle_hooks import (
|
from backend.integrations.webhooks.graph_lifecycle_hooks import on_graph_activate
|
||||||
on_graph_activate,
|
|
||||||
on_graph_deactivate,
|
|
||||||
)
|
|
||||||
from backend.util.clients import get_scheduler_client
|
from backend.util.clients import get_scheduler_client
|
||||||
from backend.util.exceptions import DatabaseError, InvalidInputError, NotFoundError
|
from backend.util.exceptions import DatabaseError, InvalidInputError, NotFoundError
|
||||||
from backend.util.json import SafeJson
|
from backend.util.json import SafeJson
|
||||||
@@ -540,92 +537,6 @@ async def update_agent_version_in_library(
|
|||||||
return library_model.LibraryAgent.from_db(lib)
|
return library_model.LibraryAgent.from_db(lib)
|
||||||
|
|
||||||
|
|
||||||
async def create_graph_in_library(
|
|
||||||
graph: graph_db.Graph,
|
|
||||||
user_id: str,
|
|
||||||
) -> tuple[graph_db.GraphModel, library_model.LibraryAgent]:
|
|
||||||
"""Create a new graph and add it to the user's library."""
|
|
||||||
graph.version = 1
|
|
||||||
graph_model = graph_db.make_graph_model(graph, user_id)
|
|
||||||
graph_model.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
|
||||||
|
|
||||||
created_graph = await graph_db.create_graph(graph_model, user_id)
|
|
||||||
|
|
||||||
library_agents = await create_library_agent(
|
|
||||||
graph=created_graph,
|
|
||||||
user_id=user_id,
|
|
||||||
sensitive_action_safe_mode=True,
|
|
||||||
create_library_agents_for_sub_graphs=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if created_graph.is_active:
|
|
||||||
created_graph = await on_graph_activate(created_graph, user_id=user_id)
|
|
||||||
|
|
||||||
return created_graph, library_agents[0]
|
|
||||||
|
|
||||||
|
|
||||||
async def update_graph_in_library(
|
|
||||||
graph: graph_db.Graph,
|
|
||||||
user_id: str,
|
|
||||||
) -> tuple[graph_db.GraphModel, library_model.LibraryAgent]:
|
|
||||||
"""Create a new version of an existing graph and update the library entry."""
|
|
||||||
existing_versions = await graph_db.get_graph_all_versions(graph.id, user_id)
|
|
||||||
current_active_version = (
|
|
||||||
next((v for v in existing_versions if v.is_active), None)
|
|
||||||
if existing_versions
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
graph.version = (
|
|
||||||
max(v.version for v in existing_versions) + 1 if existing_versions else 1
|
|
||||||
)
|
|
||||||
|
|
||||||
graph_model = graph_db.make_graph_model(graph, user_id)
|
|
||||||
graph_model.reassign_ids(user_id=user_id, reassign_graph_id=False)
|
|
||||||
|
|
||||||
created_graph = await graph_db.create_graph(graph_model, user_id)
|
|
||||||
|
|
||||||
library_agent = await get_library_agent_by_graph_id(user_id, created_graph.id)
|
|
||||||
if not library_agent:
|
|
||||||
raise NotFoundError(f"Library agent not found for graph {created_graph.id}")
|
|
||||||
|
|
||||||
library_agent = await update_library_agent_version_and_settings(
|
|
||||||
user_id, created_graph
|
|
||||||
)
|
|
||||||
|
|
||||||
if created_graph.is_active:
|
|
||||||
created_graph = await on_graph_activate(created_graph, user_id=user_id)
|
|
||||||
await graph_db.set_graph_active_version(
|
|
||||||
graph_id=created_graph.id,
|
|
||||||
version=created_graph.version,
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
if current_active_version:
|
|
||||||
await on_graph_deactivate(current_active_version, user_id=user_id)
|
|
||||||
|
|
||||||
return created_graph, library_agent
|
|
||||||
|
|
||||||
|
|
||||||
async def update_library_agent_version_and_settings(
|
|
||||||
user_id: str, agent_graph: graph_db.GraphModel
|
|
||||||
) -> library_model.LibraryAgent:
|
|
||||||
"""Update library agent to point to new graph version and sync settings."""
|
|
||||||
library = await update_agent_version_in_library(
|
|
||||||
user_id, agent_graph.id, agent_graph.version
|
|
||||||
)
|
|
||||||
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 update_library_agent(
|
|
||||||
library_agent_id=library.id,
|
|
||||||
user_id=user_id,
|
|
||||||
settings=updated_settings,
|
|
||||||
)
|
|
||||||
return library
|
|
||||||
|
|
||||||
|
|
||||||
async def update_library_agent(
|
async def update_library_agent(
|
||||||
library_agent_id: str,
|
library_agent_id: str,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ from backend.util.timezone_utils import (
|
|||||||
from backend.util.virus_scanner import scan_content_safe
|
from backend.util.virus_scanner import scan_content_safe
|
||||||
|
|
||||||
from .library import db as library_db
|
from .library import db as library_db
|
||||||
|
from .library import model as library_model
|
||||||
from .store.model import StoreAgentDetails
|
from .store.model import StoreAgentDetails
|
||||||
|
|
||||||
|
|
||||||
@@ -822,16 +823,18 @@ async def update_graph(
|
|||||||
graph: graph_db.Graph,
|
graph: graph_db.Graph,
|
||||||
user_id: Annotated[str, Security(get_user_id)],
|
user_id: Annotated[str, Security(get_user_id)],
|
||||||
) -> graph_db.GraphModel:
|
) -> graph_db.GraphModel:
|
||||||
|
# Sanity check
|
||||||
if graph.id and graph.id != graph_id:
|
if graph.id and graph.id != graph_id:
|
||||||
raise HTTPException(400, detail="Graph ID does not match ID in URI")
|
raise HTTPException(400, detail="Graph ID does not match ID in URI")
|
||||||
|
|
||||||
|
# Determine new version
|
||||||
existing_versions = await graph_db.get_graph_all_versions(graph_id, user_id=user_id)
|
existing_versions = await graph_db.get_graph_all_versions(graph_id, user_id=user_id)
|
||||||
if not existing_versions:
|
if not existing_versions:
|
||||||
raise HTTPException(404, detail=f"Graph #{graph_id} not found")
|
raise HTTPException(404, detail=f"Graph #{graph_id} not found")
|
||||||
|
latest_version_number = max(g.version for g in existing_versions)
|
||||||
|
graph.version = latest_version_number + 1
|
||||||
|
|
||||||
graph.version = max(g.version for g in existing_versions) + 1
|
|
||||||
current_active_version = next((v for v in existing_versions if v.is_active), None)
|
current_active_version = next((v for v in existing_versions if v.is_active), None)
|
||||||
|
|
||||||
graph = graph_db.make_graph_model(graph, user_id)
|
graph = graph_db.make_graph_model(graph, user_id)
|
||||||
graph.reassign_ids(user_id=user_id, reassign_graph_id=False)
|
graph.reassign_ids(user_id=user_id, reassign_graph_id=False)
|
||||||
graph.validate_graph(for_run=False)
|
graph.validate_graph(for_run=False)
|
||||||
@@ -839,23 +842,27 @@ async def update_graph(
|
|||||||
new_graph_version = await graph_db.create_graph(graph, user_id=user_id)
|
new_graph_version = await graph_db.create_graph(graph, user_id=user_id)
|
||||||
|
|
||||||
if new_graph_version.is_active:
|
if new_graph_version.is_active:
|
||||||
await library_db.update_library_agent_version_and_settings(
|
# Keep the library agent up to date with the new active version
|
||||||
user_id, new_graph_version
|
await _update_library_agent_version_and_settings(user_id, new_graph_version)
|
||||||
)
|
|
||||||
|
# Handle activation of the new graph first to ensure continuity
|
||||||
new_graph_version = await on_graph_activate(new_graph_version, user_id=user_id)
|
new_graph_version = await on_graph_activate(new_graph_version, user_id=user_id)
|
||||||
|
# Ensure new version is the only active version
|
||||||
await graph_db.set_graph_active_version(
|
await graph_db.set_graph_active_version(
|
||||||
graph_id=graph_id, version=new_graph_version.version, user_id=user_id
|
graph_id=graph_id, version=new_graph_version.version, user_id=user_id
|
||||||
)
|
)
|
||||||
if current_active_version:
|
if current_active_version:
|
||||||
|
# Handle deactivation of the previously active version
|
||||||
await on_graph_deactivate(current_active_version, user_id=user_id)
|
await on_graph_deactivate(current_active_version, user_id=user_id)
|
||||||
|
|
||||||
|
# Fetch new graph version *with sub-graphs* (needed for credentials input schema)
|
||||||
new_graph_version_with_subgraphs = await graph_db.get_graph(
|
new_graph_version_with_subgraphs = await graph_db.get_graph(
|
||||||
graph_id,
|
graph_id,
|
||||||
new_graph_version.version,
|
new_graph_version.version,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
include_subgraphs=True,
|
include_subgraphs=True,
|
||||||
)
|
)
|
||||||
assert new_graph_version_with_subgraphs
|
assert new_graph_version_with_subgraphs # make type checker happy
|
||||||
return new_graph_version_with_subgraphs
|
return new_graph_version_with_subgraphs
|
||||||
|
|
||||||
|
|
||||||
@@ -893,15 +900,33 @@ async def set_graph_active_version(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Keep the library agent up to date with the new active version
|
# Keep the library agent up to date with the new active version
|
||||||
await library_db.update_library_agent_version_and_settings(
|
await _update_library_agent_version_and_settings(user_id, new_active_graph)
|
||||||
user_id, new_active_graph
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_active_graph and current_active_graph.version != new_active_version:
|
if current_active_graph and current_active_graph.version != new_active_version:
|
||||||
# Handle deactivation of the previously active version
|
# Handle deactivation of the previously active version
|
||||||
await on_graph_deactivate(current_active_graph, user_id=user_id)
|
await on_graph_deactivate(current_active_graph, user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_library_agent_version_and_settings(
|
||||||
|
user_id: str, agent_graph: graph_db.GraphModel
|
||||||
|
) -> library_model.LibraryAgent:
|
||||||
|
library = await library_db.update_agent_version_in_library(
|
||||||
|
user_id, agent_graph.id, agent_graph.version
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
settings=updated_settings,
|
||||||
|
)
|
||||||
|
return library
|
||||||
|
|
||||||
|
|
||||||
@v1_router.patch(
|
@v1_router.patch(
|
||||||
path="/graphs/{graph_id}/settings",
|
path="/graphs/{graph_id}/settings",
|
||||||
summary="Update graph settings",
|
summary="Update graph settings",
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
"""Text encoding block for converting special characters to escape sequences."""
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
from backend.data.block import (
|
|
||||||
Block,
|
|
||||||
BlockCategory,
|
|
||||||
BlockOutput,
|
|
||||||
BlockSchemaInput,
|
|
||||||
BlockSchemaOutput,
|
|
||||||
)
|
|
||||||
from backend.data.model import SchemaField
|
|
||||||
|
|
||||||
|
|
||||||
class TextEncoderBlock(Block):
|
|
||||||
"""
|
|
||||||
Encodes a string by converting special characters into escape sequences.
|
|
||||||
|
|
||||||
This block is the inverse of TextDecoderBlock. It takes text containing
|
|
||||||
special characters (like newlines, tabs, etc.) and converts them into
|
|
||||||
their escape sequence representations (e.g., newline becomes \\n).
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Input(BlockSchemaInput):
|
|
||||||
"""Input schema for TextEncoderBlock."""
|
|
||||||
|
|
||||||
text: str = SchemaField(
|
|
||||||
description="A string containing special characters to be encoded",
|
|
||||||
placeholder="Your text with newlines and quotes to encode",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Output(BlockSchemaOutput):
|
|
||||||
"""Output schema for TextEncoderBlock."""
|
|
||||||
|
|
||||||
encoded_text: str = SchemaField(
|
|
||||||
description="The encoded text with special characters converted to escape sequences"
|
|
||||||
)
|
|
||||||
error: str = SchemaField(description="Error message if encoding fails")
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
id="5185f32e-4b65-4ecf-8fbb-873f003f09d6",
|
|
||||||
description="Encodes a string by converting special characters into escape sequences",
|
|
||||||
categories={BlockCategory.TEXT},
|
|
||||||
input_schema=TextEncoderBlock.Input,
|
|
||||||
output_schema=TextEncoderBlock.Output,
|
|
||||||
test_input={
|
|
||||||
"text": """Hello
|
|
||||||
World!
|
|
||||||
This is a "quoted" string."""
|
|
||||||
},
|
|
||||||
test_output=[
|
|
||||||
(
|
|
||||||
"encoded_text",
|
|
||||||
"""Hello\\nWorld!\\nThis is a "quoted" string.""",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
|
||||||
"""
|
|
||||||
Encode the input text by converting special characters to escape sequences.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_data: The input containing the text to encode.
|
|
||||||
**kwargs: Additional keyword arguments (unused).
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
The encoded text with escape sequences, or an error message if encoding fails.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
encoded_text = codecs.encode(input_data.text, "unicode_escape").decode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
yield "encoded_text", encoded_text
|
|
||||||
except Exception as e:
|
|
||||||
yield "error", f"Encoding error: {str(e)}"
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from backend.blocks.encoder_block import TextEncoderBlock
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_text_encoder_basic():
|
|
||||||
"""Test basic encoding of newlines and special characters."""
|
|
||||||
block = TextEncoderBlock()
|
|
||||||
result = []
|
|
||||||
async for output in block.run(TextEncoderBlock.Input(text="Hello\nWorld")):
|
|
||||||
result.append(output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0][0] == "encoded_text"
|
|
||||||
assert result[0][1] == "Hello\\nWorld"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_text_encoder_multiple_escapes():
|
|
||||||
"""Test encoding of multiple escape sequences."""
|
|
||||||
block = TextEncoderBlock()
|
|
||||||
result = []
|
|
||||||
async for output in block.run(
|
|
||||||
TextEncoderBlock.Input(text="Line1\nLine2\tTabbed\rCarriage")
|
|
||||||
):
|
|
||||||
result.append(output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0][0] == "encoded_text"
|
|
||||||
assert "\\n" in result[0][1]
|
|
||||||
assert "\\t" in result[0][1]
|
|
||||||
assert "\\r" in result[0][1]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_text_encoder_unicode():
|
|
||||||
"""Test that unicode characters are handled correctly."""
|
|
||||||
block = TextEncoderBlock()
|
|
||||||
result = []
|
|
||||||
async for output in block.run(TextEncoderBlock.Input(text="Hello 世界\n")):
|
|
||||||
result.append(output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0][0] == "encoded_text"
|
|
||||||
# Unicode characters should be escaped as \uXXXX sequences
|
|
||||||
assert "\\n" in result[0][1]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_text_encoder_empty_string():
|
|
||||||
"""Test encoding of an empty string."""
|
|
||||||
block = TextEncoderBlock()
|
|
||||||
result = []
|
|
||||||
async for output in block.run(TextEncoderBlock.Input(text="")):
|
|
||||||
result.append(output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0][0] == "encoded_text"
|
|
||||||
assert result[0][1] == ""
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_text_encoder_error_handling():
|
|
||||||
"""Test that encoding errors are handled gracefully."""
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
block = TextEncoderBlock()
|
|
||||||
result = []
|
|
||||||
|
|
||||||
with patch("codecs.encode", side_effect=Exception("Mocked encoding error")):
|
|
||||||
async for output in block.run(TextEncoderBlock.Input(text="test")):
|
|
||||||
result.append(output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0][0] == "error"
|
|
||||||
assert "Mocked encoding error" in result[0][1]
|
|
||||||
@@ -165,13 +165,10 @@ class TranscribeYoutubeVideoBlock(Block):
|
|||||||
credentials: WebshareProxyCredentials,
|
credentials: WebshareProxyCredentials,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> BlockOutput:
|
) -> BlockOutput:
|
||||||
try:
|
video_id = self.extract_video_id(input_data.youtube_url)
|
||||||
video_id = self.extract_video_id(input_data.youtube_url)
|
yield "video_id", video_id
|
||||||
transcript = self.get_transcript(video_id, credentials)
|
|
||||||
transcript_text = self.format_transcript(transcript=transcript)
|
|
||||||
|
|
||||||
# Only yield after all operations succeed
|
transcript = self.get_transcript(video_id, credentials)
|
||||||
yield "video_id", video_id
|
transcript_text = self.format_transcript(transcript=transcript)
|
||||||
yield "transcript", transcript_text
|
|
||||||
except Exception as e:
|
yield "transcript", transcript_text
|
||||||
yield "error", str(e)
|
|
||||||
|
|||||||
@@ -134,16 +134,6 @@ async def test_block_credit_reset(server: SpinTestServer):
|
|||||||
month1 = datetime.now(timezone.utc).replace(month=1, day=1)
|
month1 = datetime.now(timezone.utc).replace(month=1, day=1)
|
||||||
user_credit.time_now = lambda: month1
|
user_credit.time_now = lambda: month1
|
||||||
|
|
||||||
# IMPORTANT: Set updatedAt to December of previous year to ensure it's
|
|
||||||
# in a different month than month1 (January). This fixes a timing bug
|
|
||||||
# where if the test runs in early February, 35 days ago would be January,
|
|
||||||
# matching the mocked month1 and preventing the refill from triggering.
|
|
||||||
dec_previous_year = month1.replace(year=month1.year - 1, month=12, day=15)
|
|
||||||
await UserBalance.prisma().update(
|
|
||||||
where={"userId": DEFAULT_USER_ID},
|
|
||||||
data={"updatedAt": dec_previous_year},
|
|
||||||
)
|
|
||||||
|
|
||||||
# First call in month 1 should trigger refill
|
# First call in month 1 should trigger refill
|
||||||
balance = await user_credit.get_credits(DEFAULT_USER_ID)
|
balance = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
assert balance == REFILL_VALUE # Should get 1000 credits
|
assert balance == REFILL_VALUE # Should get 1000 credits
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from pydantic import SecretStr
|
|||||||
from sentry_sdk.integrations import DidNotEnable
|
from sentry_sdk.integrations import DidNotEnable
|
||||||
from sentry_sdk.integrations.anthropic import AnthropicIntegration
|
from sentry_sdk.integrations.anthropic import AnthropicIntegration
|
||||||
from sentry_sdk.integrations.asyncio import AsyncioIntegration
|
from sentry_sdk.integrations.asyncio import AsyncioIntegration
|
||||||
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||||
|
from sentry_sdk.integrations.httpx import HttpxIntegration
|
||||||
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration
|
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration
|
||||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||||
|
|
||||||
@@ -37,6 +39,8 @@ def sentry_init():
|
|||||||
_experiments={"enable_logs": True},
|
_experiments={"enable_logs": True},
|
||||||
integrations=[
|
integrations=[
|
||||||
AsyncioIntegration(),
|
AsyncioIntegration(),
|
||||||
|
FastApiIntegration(), # Traces FastAPI requests with detailed spans
|
||||||
|
HttpxIntegration(), # Traces outgoing HTTP calls (OpenAI, external APIs)
|
||||||
LoggingIntegration(sentry_logs_level=logging.INFO),
|
LoggingIntegration(sentry_logs_level=logging.INFO),
|
||||||
AnthropicIntegration(
|
AnthropicIntegration(
|
||||||
include_prompts=False,
|
include_prompts=False,
|
||||||
|
|||||||
@@ -346,7 +346,6 @@ export function ChatMessage({
|
|||||||
toolId={message.toolId}
|
toolId={message.toolId}
|
||||||
toolName={message.toolName}
|
toolName={message.toolName}
|
||||||
result={message.result}
|
result={message.result}
|
||||||
onSendMessage={onSendMessage}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export function MessageList({
|
|||||||
key={index}
|
key={index}
|
||||||
message={message}
|
message={message}
|
||||||
prevMessage={messages[index - 1]}
|
prevMessage={messages[index - 1]}
|
||||||
onSendMessage={onSendMessage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import { shouldSkipAgentOutput } from "../../helpers";
|
|||||||
export interface LastToolResponseProps {
|
export interface LastToolResponseProps {
|
||||||
message: ChatMessageData;
|
message: ChatMessageData;
|
||||||
prevMessage: ChatMessageData | undefined;
|
prevMessage: ChatMessageData | undefined;
|
||||||
onSendMessage?: (content: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LastToolResponse({
|
export function LastToolResponse({
|
||||||
message,
|
message,
|
||||||
prevMessage,
|
prevMessage,
|
||||||
onSendMessage,
|
|
||||||
}: LastToolResponseProps) {
|
}: LastToolResponseProps) {
|
||||||
if (message.type !== "tool_response") return null;
|
if (message.type !== "tool_response") return null;
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ export function LastToolResponse({
|
|||||||
toolId={message.toolId}
|
toolId={message.toolId}
|
||||||
toolName={message.toolName}
|
toolName={message.toolName}
|
||||||
result={message.result}
|
result={message.result}
|
||||||
onSendMessage={onSendMessage}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Progress } from "@/components/atoms/Progress/Progress";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
||||||
import { useAsymptoticProgress } from "../ToolCallMessage/useAsymptoticProgress";
|
|
||||||
|
|
||||||
export interface ThinkingMessageProps {
|
export interface ThinkingMessageProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -13,19 +11,18 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
|
|||||||
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
|
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
|
||||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const coffeeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const coffeeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const progress = useAsymptoticProgress(showCoffeeMessage);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timerRef.current === null) {
|
if (timerRef.current === null) {
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setShowSlowLoader(true);
|
setShowSlowLoader(true);
|
||||||
}, 3000);
|
}, 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coffeeTimerRef.current === null) {
|
if (coffeeTimerRef.current === null) {
|
||||||
coffeeTimerRef.current = setTimeout(() => {
|
coffeeTimerRef.current = setTimeout(() => {
|
||||||
setShowCoffeeMessage(true);
|
setShowCoffeeMessage(true);
|
||||||
}, 8000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -52,18 +49,9 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
|
|||||||
<AIChatBubble>
|
<AIChatBubble>
|
||||||
<div className="transition-all duration-500 ease-in-out">
|
<div className="transition-all duration-500 ease-in-out">
|
||||||
{showCoffeeMessage ? (
|
{showCoffeeMessage ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||||
<div className="flex w-full max-w-[280px] flex-col gap-1.5">
|
This could take a few minutes, grab a coffee ☕️
|
||||||
<div className="flex items-center justify-between text-xs text-neutral-500">
|
</span>
|
||||||
<span>Working on it...</span>
|
|
||||||
<span>{Math.round(progress)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={progress} className="h-2 w-full" />
|
|
||||||
</div>
|
|
||||||
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
|
||||||
This could take a few minutes, grab a coffee ☕️
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : showSlowLoader ? (
|
) : showSlowLoader ? (
|
||||||
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||||
Taking a bit more time...
|
Taking a bit more time...
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook that returns a progress value that starts fast and slows down,
|
|
||||||
* asymptotically approaching but never reaching the max value.
|
|
||||||
*
|
|
||||||
* Uses a half-life formula: progress = max * (1 - 0.5^(time/halfLife))
|
|
||||||
* This creates the "game loading bar" effect where:
|
|
||||||
* - 50% is reached at halfLifeSeconds
|
|
||||||
* - 75% is reached at 2 * halfLifeSeconds
|
|
||||||
* - 87.5% is reached at 3 * halfLifeSeconds
|
|
||||||
* - and so on...
|
|
||||||
*
|
|
||||||
* @param isActive - Whether the progress should be animating
|
|
||||||
* @param halfLifeSeconds - Time in seconds to reach 50% progress (default: 30)
|
|
||||||
* @param maxProgress - Maximum progress value to approach (default: 100)
|
|
||||||
* @param intervalMs - Update interval in milliseconds (default: 100)
|
|
||||||
* @returns Current progress value (0-maxProgress)
|
|
||||||
*/
|
|
||||||
export function useAsymptoticProgress(
|
|
||||||
isActive: boolean,
|
|
||||||
halfLifeSeconds = 30,
|
|
||||||
maxProgress = 100,
|
|
||||||
intervalMs = 100,
|
|
||||||
) {
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
const elapsedTimeRef = useRef(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActive) {
|
|
||||||
setProgress(0);
|
|
||||||
elapsedTimeRef.current = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
elapsedTimeRef.current += intervalMs / 1000;
|
|
||||||
// Half-life approach: progress = max * (1 - 0.5^(time/halfLife))
|
|
||||||
// At t=halfLife: 50%, at t=2*halfLife: 75%, at t=3*halfLife: 87.5%, etc.
|
|
||||||
const newProgress =
|
|
||||||
maxProgress *
|
|
||||||
(1 - Math.pow(0.5, elapsedTimeRef.current / halfLifeSeconds));
|
|
||||||
setProgress(newProgress);
|
|
||||||
}, intervalMs);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [isActive, halfLifeSeconds, maxProgress, intervalMs]);
|
|
||||||
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
|
|
||||||
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
|
||||||
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
|
|
||||||
import { RunAgentModal } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal";
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
|
||||||
import {
|
|
||||||
CheckCircleIcon,
|
|
||||||
PencilLineIcon,
|
|
||||||
PlayIcon,
|
|
||||||
} from "@phosphor-icons/react";
|
|
||||||
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
agentName: string;
|
|
||||||
libraryAgentId: string;
|
|
||||||
onSendMessage?: (content: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AgentCreatedPrompt({
|
|
||||||
agentName,
|
|
||||||
libraryAgentId,
|
|
||||||
onSendMessage,
|
|
||||||
}: Props) {
|
|
||||||
// Fetch library agent eagerly so modal is ready when user clicks
|
|
||||||
const { data: libraryAgentResponse, isLoading } = useGetV2GetLibraryAgent(
|
|
||||||
libraryAgentId,
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
enabled: !!libraryAgentId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const libraryAgent =
|
|
||||||
libraryAgentResponse?.status === 200 ? libraryAgentResponse.data : null;
|
|
||||||
|
|
||||||
function handleRunWithPlaceholders() {
|
|
||||||
onSendMessage?.(
|
|
||||||
`Run the agent "${agentName}" with placeholder/example values so I can test it.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRunCreated(execution: GraphExecutionMeta) {
|
|
||||||
onSendMessage?.(
|
|
||||||
`I've started the agent "${agentName}". The execution ID is ${execution.id}. Please monitor its progress and let me know when it completes.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleScheduleCreated(schedule: GraphExecutionJobInfo) {
|
|
||||||
const scheduleInfo = schedule.cron
|
|
||||||
? `with cron schedule "${schedule.cron}"`
|
|
||||||
: "to run on the specified schedule";
|
|
||||||
onSendMessage?.(
|
|
||||||
`I've scheduled the agent "${agentName}" ${scheduleInfo}. The schedule ID is ${schedule.id}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AIChatBubble>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100">
|
|
||||||
<CheckCircleIcon
|
|
||||||
size={18}
|
|
||||||
weight="fill"
|
|
||||||
className="text-green-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Text variant="body-medium" className="text-neutral-900">
|
|
||||||
Agent Created Successfully
|
|
||||||
</Text>
|
|
||||||
<Text variant="small" className="text-neutral-500">
|
|
||||||
"{agentName}" is ready to test
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Text variant="small-medium" className="text-neutral-700">
|
|
||||||
Ready to test?
|
|
||||||
</Text>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="small"
|
|
||||||
onClick={handleRunWithPlaceholders}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<PlayIcon size={16} />
|
|
||||||
Run with example values
|
|
||||||
</Button>
|
|
||||||
{libraryAgent ? (
|
|
||||||
<RunAgentModal
|
|
||||||
triggerSlot={
|
|
||||||
<Button variant="outline" size="small" className="gap-2">
|
|
||||||
<PencilLineIcon size={16} />
|
|
||||||
Run with my inputs
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
agent={libraryAgent}
|
|
||||||
onRunCreated={handleRunCreated}
|
|
||||||
onScheduleCreated={handleScheduleCreated}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="small"
|
|
||||||
loading={isLoading}
|
|
||||||
disabled
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<PencilLineIcon size={16} />
|
|
||||||
Run with my inputs
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Text variant="small" className="text-neutral-500">
|
|
||||||
or just ask me
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AIChatBubble>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,11 @@ import { Text } from "@/components/atoms/Text/Text";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ToolResult } from "@/types/chat";
|
import type { ToolResult } from "@/types/chat";
|
||||||
import { WarningCircleIcon } from "@phosphor-icons/react";
|
import { WarningCircleIcon } from "@phosphor-icons/react";
|
||||||
import { AgentCreatedPrompt } from "./AgentCreatedPrompt";
|
|
||||||
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
||||||
import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
|
import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
|
||||||
import {
|
import {
|
||||||
formatToolResponse,
|
formatToolResponse,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
isAgentSavedResponse,
|
|
||||||
isErrorResponse,
|
isErrorResponse,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
@@ -18,7 +16,6 @@ export interface ToolResponseMessageProps {
|
|||||||
result?: ToolResult;
|
result?: ToolResult;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onSendMessage?: (content: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolResponseMessage({
|
export function ToolResponseMessage({
|
||||||
@@ -27,7 +24,6 @@ export function ToolResponseMessage({
|
|||||||
result,
|
result,
|
||||||
success: _success,
|
success: _success,
|
||||||
className,
|
className,
|
||||||
onSendMessage,
|
|
||||||
}: ToolResponseMessageProps) {
|
}: ToolResponseMessageProps) {
|
||||||
if (isErrorResponse(result)) {
|
if (isErrorResponse(result)) {
|
||||||
const errorMessage = getErrorMessage(result);
|
const errorMessage = getErrorMessage(result);
|
||||||
@@ -47,18 +43,6 @@ export function ToolResponseMessage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for agent_saved response - show special prompt
|
|
||||||
const agentSavedData = isAgentSavedResponse(result);
|
|
||||||
if (agentSavedData.isSaved) {
|
|
||||||
return (
|
|
||||||
<AgentCreatedPrompt
|
|
||||||
agentName={agentSavedData.agentName}
|
|
||||||
libraryAgentId={agentSavedData.libraryAgentId}
|
|
||||||
onSendMessage={onSendMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedText = formatToolResponse(result, toolName);
|
const formattedText = formatToolResponse(result, toolName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,43 +6,6 @@ function stripInternalReasoning(content: string): string {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentSavedData {
|
|
||||||
isSaved: boolean;
|
|
||||||
agentName: string;
|
|
||||||
agentId: string;
|
|
||||||
libraryAgentId: string;
|
|
||||||
libraryAgentLink: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAgentSavedResponse(result: unknown): AgentSavedData {
|
|
||||||
if (typeof result !== "object" || result === null) {
|
|
||||||
return {
|
|
||||||
isSaved: false,
|
|
||||||
agentName: "",
|
|
||||||
agentId: "",
|
|
||||||
libraryAgentId: "",
|
|
||||||
libraryAgentLink: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const response = result as Record<string, unknown>;
|
|
||||||
if (response.type === "agent_saved") {
|
|
||||||
return {
|
|
||||||
isSaved: true,
|
|
||||||
agentName: (response.agent_name as string) || "Agent",
|
|
||||||
agentId: (response.agent_id as string) || "",
|
|
||||||
libraryAgentId: (response.library_agent_id as string) || "",
|
|
||||||
libraryAgentLink: (response.library_agent_link as string) || "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isSaved: false,
|
|
||||||
agentName: "",
|
|
||||||
agentId: "",
|
|
||||||
libraryAgentId: "",
|
|
||||||
libraryAgentLink: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isErrorResponse(result: unknown): boolean {
|
export function isErrorResponse(result: unknown): boolean {
|
||||||
if (typeof result === "string") {
|
if (typeof result === "string") {
|
||||||
const lower = result.toLowerCase();
|
const lower = result.toLowerCase();
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ Below is a comprehensive list of all available blocks, categorized by their prim
|
|||||||
| [Get Current Time](block-integrations/text.md#get-current-time) | This block outputs the current time |
|
| [Get Current Time](block-integrations/text.md#get-current-time) | This block outputs the current time |
|
||||||
| [Match Text Pattern](block-integrations/text.md#match-text-pattern) | Matches text against a regex pattern and forwards data to positive or negative output based on the match |
|
| [Match Text Pattern](block-integrations/text.md#match-text-pattern) | Matches text against a regex pattern and forwards data to positive or negative output based on the match |
|
||||||
| [Text Decoder](block-integrations/text.md#text-decoder) | Decodes a string containing escape sequences into actual text |
|
| [Text Decoder](block-integrations/text.md#text-decoder) | Decodes a string containing escape sequences into actual text |
|
||||||
| [Text Encoder](block-integrations/text.md#text-encoder) | Encodes a string by converting special characters into escape sequences |
|
|
||||||
| [Text Replace](block-integrations/text.md#text-replace) | This block is used to replace a text with a new text |
|
| [Text Replace](block-integrations/text.md#text-replace) | This block is used to replace a text with a new text |
|
||||||
| [Text Split](block-integrations/text.md#text-split) | This block is used to split a text into a list of strings |
|
| [Text Split](block-integrations/text.md#text-split) | This block is used to split a text into a list of strings |
|
||||||
| [Word Character Count](block-integrations/text.md#word-character-count) | Counts the number of words and characters in a given text |
|
| [Word Character Count](block-integrations/text.md#word-character-count) | Counts the number of words and characters in a given text |
|
||||||
|
|||||||
@@ -380,42 +380,6 @@ This is useful when working with data from APIs or files where escape sequences
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Text Encoder
|
|
||||||
|
|
||||||
### What it is
|
|
||||||
Encodes a string by converting special characters into escape sequences
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
<!-- MANUAL: how_it_works -->
|
|
||||||
The Text Encoder takes the input string and applies Python's `unicode_escape` encoding (equivalent to `codecs.encode(text, "unicode_escape").decode("utf-8")`) to transform special characters like newlines, tabs, and backslashes into their escaped forms.
|
|
||||||
|
|
||||||
The block relies on the input schema to ensure the value is a string; non-string inputs are rejected by validation, and any encoding failures surface as block errors. Non-ASCII characters are emitted as `\uXXXX` sequences, which is useful for ASCII-only payloads.
|
|
||||||
<!-- END MANUAL -->
|
|
||||||
|
|
||||||
### Inputs
|
|
||||||
|
|
||||||
| Input | Description | Type | Required |
|
|
||||||
|-------|-------------|------|----------|
|
|
||||||
| text | A string containing special characters to be encoded | str | Yes |
|
|
||||||
|
|
||||||
### Outputs
|
|
||||||
|
|
||||||
| Output | Description | Type |
|
|
||||||
|--------|-------------|------|
|
|
||||||
| error | Error message if encoding fails | str |
|
|
||||||
| encoded_text | The encoded text with special characters converted to escape sequences | str |
|
|
||||||
|
|
||||||
### Possible use case
|
|
||||||
<!-- MANUAL: use_case -->
|
|
||||||
**JSON Payload Preparation**: Encode multiline or quoted text before embedding it in JSON string fields to ensure proper escaping.
|
|
||||||
|
|
||||||
**Config/ENV Generation**: Convert template text into escaped strings for `.env` or YAML values that require special character handling.
|
|
||||||
|
|
||||||
**Snapshot Fixtures**: Produce stable escaped strings for golden files or API tests where consistent text representation is needed.
|
|
||||||
<!-- END MANUAL -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Text Replace
|
## Text Replace
|
||||||
|
|
||||||
### What it is
|
### What it is
|
||||||
|
|||||||
Reference in New Issue
Block a user