mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-14 01:28:11 -05:00
Compare commits
14 Commits
hackathon-
...
fix/execut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fc2e3bbd8 | ||
|
|
3b6dc48033 | ||
|
|
3cab0c1240 | ||
|
|
2416975c30 | ||
|
|
bb8aab7bd4 | ||
|
|
a04b891e1c | ||
|
|
a304332bea | ||
|
|
01cfac9d5a | ||
|
|
f482eb668b | ||
|
|
fc8434fb30 | ||
|
|
3ae08cd48e | ||
|
|
4db13837b9 | ||
|
|
df87867625 | ||
|
|
4a7bc006a8 |
@@ -28,6 +28,7 @@ from backend.executor.manager import get_db_async_client
|
||||
from backend.util.settings import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class ExecutionAnalyticsRequest(BaseModel):
|
||||
@@ -63,6 +64,8 @@ class ExecutionAnalyticsResult(BaseModel):
|
||||
score: Optional[float]
|
||||
status: str # "success", "failed", "skipped"
|
||||
error_message: Optional[str] = None
|
||||
started_at: Optional[datetime] = None
|
||||
ended_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class ExecutionAnalyticsResponse(BaseModel):
|
||||
@@ -224,11 +227,6 @@ async def generate_execution_analytics(
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate model configuration
|
||||
settings = Settings()
|
||||
if not settings.secrets.openai_internal_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API key not configured")
|
||||
|
||||
# Get database client
|
||||
db_client = get_db_async_client()
|
||||
|
||||
@@ -320,6 +318,8 @@ async def generate_execution_analytics(
|
||||
),
|
||||
status="skipped",
|
||||
error_message=None, # Not an error - just already processed
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -349,6 +349,9 @@ async def _process_batch(
|
||||
) -> list[ExecutionAnalyticsResult]:
|
||||
"""Process a batch of executions concurrently."""
|
||||
|
||||
if not settings.secrets.openai_internal_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API key not configured")
|
||||
|
||||
async def process_single_execution(execution) -> ExecutionAnalyticsResult:
|
||||
try:
|
||||
# Generate activity status and score using the specified model
|
||||
@@ -387,6 +390,8 @@ async def _process_batch(
|
||||
score=None,
|
||||
status="skipped",
|
||||
error_message="Activity generation returned None",
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
|
||||
# Update the execution stats
|
||||
@@ -416,6 +421,8 @@ async def _process_batch(
|
||||
summary_text=activity_response["activity_status"],
|
||||
score=activity_response["correctness_score"],
|
||||
status="success",
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -429,6 +436,8 @@ async def _process_batch(
|
||||
score=None,
|
||||
status="failed",
|
||||
error_message=str(e),
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
|
||||
# Process all executions in the batch concurrently
|
||||
|
||||
@@ -495,8 +495,14 @@ class SmartDecisionMakerBlock(Block):
|
||||
}
|
||||
|
||||
properties = {}
|
||||
field_mapping = {}
|
||||
|
||||
for link in links:
|
||||
field_name = link.sink_name
|
||||
|
||||
clean_field_name = SmartDecisionMakerBlock.cleanup(field_name)
|
||||
field_mapping[clean_field_name] = field_name
|
||||
|
||||
sink_block_input_schema = sink_node.input_default["input_schema"]
|
||||
sink_block_properties = sink_block_input_schema.get("properties", {}).get(
|
||||
link.sink_name, {}
|
||||
@@ -506,7 +512,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
if "description" in sink_block_properties
|
||||
else f"The {link.sink_name} of the tool"
|
||||
)
|
||||
properties[link.sink_name] = {
|
||||
properties[clean_field_name] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
"default": json.dumps(sink_block_properties.get("default", None)),
|
||||
@@ -519,7 +525,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
# Store node info for later use in output processing
|
||||
tool_function["_field_mapping"] = field_mapping
|
||||
tool_function["_sink_node_id"] = sink_node.id
|
||||
|
||||
return {"type": "function", "function": tool_function}
|
||||
@@ -1129,8 +1135,9 @@ class SmartDecisionMakerBlock(Block):
|
||||
original_field_name = field_mapping.get(clean_arg_name, clean_arg_name)
|
||||
arg_value = tool_args.get(clean_arg_name)
|
||||
|
||||
sanitized_arg_name = self.cleanup(original_field_name)
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{sanitized_arg_name}"
|
||||
# Use original_field_name directly (not sanitized) to match link sink_name
|
||||
# The field_mapping already translates from LLM's cleaned names to original names
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{original_field_name}"
|
||||
|
||||
logger.debug(
|
||||
"[SmartDecisionMakerBlock|geid:%s|neid:%s] emit %s",
|
||||
|
||||
@@ -104,7 +104,7 @@ async def get_accuracy_trends_and_alerts(
|
||||
AND e."executionStatus" IN ('COMPLETED', 'FAILED', 'TERMINATED')
|
||||
{user_filter}
|
||||
GROUP BY DATE(e."createdAt")
|
||||
HAVING COUNT(*) >= 3 -- Need at least 3 executions per day
|
||||
HAVING COUNT(*) >= 1 -- Include all days with at least 1 execution
|
||||
),
|
||||
trends AS (
|
||||
SELECT
|
||||
|
||||
@@ -153,8 +153,14 @@ class GraphExecutionMeta(BaseDbModel):
|
||||
nodes_input_masks: Optional[dict[str, BlockInput]]
|
||||
preset_id: Optional[str]
|
||||
status: ExecutionStatus
|
||||
started_at: datetime
|
||||
ended_at: datetime
|
||||
started_at: Optional[datetime] = Field(
|
||||
None,
|
||||
description="When execution started running. Null if not yet started (QUEUED).",
|
||||
)
|
||||
ended_at: Optional[datetime] = Field(
|
||||
None,
|
||||
description="When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW).",
|
||||
)
|
||||
is_shared: bool = False
|
||||
share_token: Optional[str] = None
|
||||
|
||||
@@ -229,10 +235,8 @@ class GraphExecutionMeta(BaseDbModel):
|
||||
|
||||
@staticmethod
|
||||
def from_db(_graph_exec: AgentGraphExecution):
|
||||
now = datetime.now(timezone.utc)
|
||||
# TODO: make started_at and ended_at optional
|
||||
start_time = _graph_exec.startedAt or _graph_exec.createdAt
|
||||
end_time = _graph_exec.updatedAt or now
|
||||
start_time = _graph_exec.startedAt
|
||||
end_time = _graph_exec.endedAt
|
||||
|
||||
try:
|
||||
stats = GraphExecutionStats.model_validate(_graph_exec.stats)
|
||||
@@ -900,6 +904,14 @@ async def update_graph_execution_stats(
|
||||
|
||||
if status:
|
||||
update_data["executionStatus"] = status
|
||||
# Set endedAt when execution reaches a terminal status
|
||||
terminal_statuses = [
|
||||
ExecutionStatus.COMPLETED,
|
||||
ExecutionStatus.FAILED,
|
||||
ExecutionStatus.TERMINATED,
|
||||
]
|
||||
if status in terminal_statuses:
|
||||
update_data["endedAt"] = datetime.now(tz=timezone.utc)
|
||||
|
||||
where_clause: AgentGraphExecutionWhereInput = {"id": graph_exec_id}
|
||||
|
||||
|
||||
@@ -60,8 +60,10 @@ class LateExecutionMonitor:
|
||||
if not all_late_executions:
|
||||
return "No late executions detected."
|
||||
|
||||
# Sort by created time (oldest first)
|
||||
all_late_executions.sort(key=lambda x: x.started_at)
|
||||
# Sort by started time (oldest first), with None values (unstarted) first
|
||||
all_late_executions.sort(
|
||||
key=lambda x: x.started_at or datetime.min.replace(tzinfo=timezone.utc)
|
||||
)
|
||||
|
||||
num_total_late = len(all_late_executions)
|
||||
num_queued = len(queued_late_executions)
|
||||
@@ -74,7 +76,7 @@ class LateExecutionMonitor:
|
||||
was_truncated = num_total_late > tuncate_size
|
||||
|
||||
late_execution_details = [
|
||||
f"* `Execution ID: {exec.id}, Graph ID: {exec.graph_id}v{exec.graph_version}, User ID: {exec.user_id}, Status: {exec.status}, Created At: {exec.started_at.isoformat()}`"
|
||||
f"* `Execution ID: {exec.id}, Graph ID: {exec.graph_id}v{exec.graph_version}, User ID: {exec.user_id}, Status: {exec.status}, Started At: {exec.started_at.isoformat() if exec.started_at else 'Not started'}`"
|
||||
for exec in truncated_executions
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentGraphExecution" ADD COLUMN "endedAt" TIMESTAMP(3);
|
||||
|
||||
-- Set endedAt to updatedAt for existing records with terminal status only
|
||||
UPDATE "AgentGraphExecution"
|
||||
SET "endedAt" = "updatedAt"
|
||||
WHERE "endedAt" IS NULL
|
||||
AND "executionStatus" IN ('COMPLETED', 'FAILED', 'TERMINATED');
|
||||
@@ -383,6 +383,7 @@ model AgentGraphExecution {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime? @updatedAt
|
||||
startedAt DateTime?
|
||||
endedAt DateTime?
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@rjsf/core": "6.1.2",
|
||||
"@rjsf/utils": "6.1.2",
|
||||
"@rjsf/validator-ajv8": "5.24.13",
|
||||
"@rjsf/validator-ajv8": "6.1.2",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@supabase/ssr": "0.7.0",
|
||||
"@supabase/supabase-js": "2.78.0",
|
||||
|
||||
3640
autogpt_platform/frontend/pnpm-lock.yaml
generated
3640
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,8 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
"Execution ID",
|
||||
"Status",
|
||||
"Score",
|
||||
"Started At",
|
||||
"Ended At",
|
||||
"Summary Text",
|
||||
"Error Message",
|
||||
];
|
||||
@@ -62,6 +64,8 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
result.exec_id,
|
||||
result.status,
|
||||
result.score?.toString() || "",
|
||||
result.started_at ? new Date(result.started_at).toLocaleString() : "",
|
||||
result.ended_at ? new Date(result.ended_at).toLocaleString() : "",
|
||||
`"${(result.summary_text || "").replace(/"/g, '""')}"`, // Escape quotes in summary
|
||||
`"${(result.error_message || "").replace(/"/g, '""')}"`, // Escape quotes in error
|
||||
]);
|
||||
@@ -248,15 +252,13 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{(result.summary_text || result.error_message) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => toggleRowExpansion(result.exec_id)}
|
||||
>
|
||||
<EyeIcon size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => toggleRowExpansion(result.exec_id)}
|
||||
>
|
||||
<EyeIcon size={16} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -264,6 +266,44 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
<tr>
|
||||
<td colSpan={7} className="bg-gray-50 px-4 py-3">
|
||||
<div className="space-y-3">
|
||||
{/* Timestamps section */}
|
||||
<div className="grid grid-cols-2 gap-4 border-b border-gray-200 pb-3">
|
||||
<div>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-xs font-medium text-gray-600"
|
||||
>
|
||||
Started At:
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-sm text-gray-700"
|
||||
>
|
||||
{result.started_at
|
||||
? new Date(
|
||||
result.started_at,
|
||||
).toLocaleString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-xs font-medium text-gray-600"
|
||||
>
|
||||
Ended At:
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-sm text-gray-700"
|
||||
>
|
||||
{result.ended_at
|
||||
? new Date(result.ended_at).toLocaleString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result.summary_text && (
|
||||
<div>
|
||||
<Text
|
||||
|
||||
@@ -541,7 +541,19 @@ export function ExecutionAnalyticsForm() {
|
||||
{/* Accuracy Trends Display */}
|
||||
{trendsData && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Execution Accuracy Trends</h3>
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-lg font-semibold">Execution Accuracy Trends</h3>
|
||||
<div className="rounded-md bg-blue-50 px-3 py-2 text-xs text-blue-700">
|
||||
<p className="font-medium">
|
||||
Chart Filters (matches monitoring system):
|
||||
</p>
|
||||
<ul className="mt-1 list-inside list-disc space-y-1">
|
||||
<li>Only days with ≥1 execution with correctness score</li>
|
||||
<li>Last 30 days</li>
|
||||
<li>Averages calculated from scored executions only</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert Section */}
|
||||
{trendsData.alert && (
|
||||
|
||||
@@ -173,8 +173,9 @@ export function OldAgentLibraryView() {
|
||||
if (agentRuns.length > 0) {
|
||||
// select latest run
|
||||
const latestRun = agentRuns.reduce((latest, current) => {
|
||||
if (latest.started_at && !current.started_at) return current;
|
||||
else if (!latest.started_at) return latest;
|
||||
if (!latest.started_at && !current.started_at) return latest;
|
||||
if (!latest.started_at) return current;
|
||||
if (!current.started_at) return latest;
|
||||
return latest.started_at > current.started_at ? latest : current;
|
||||
}, agentRuns[0]);
|
||||
selectRun(latestRun.id as GraphExecutionID);
|
||||
|
||||
@@ -184,9 +184,11 @@ export function AgentRunsSelectorList({
|
||||
))}
|
||||
{agentPresets.length > 0 && <Separator className="my-1" />}
|
||||
{agentRuns
|
||||
.toSorted(
|
||||
(a, b) => b.started_at.getTime() - a.started_at.getTime(),
|
||||
)
|
||||
.toSorted((a, b) => {
|
||||
const aTime = a.started_at?.getTime() ?? 0;
|
||||
const bTime = b.started_at?.getTime() ?? 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.map((run) => (
|
||||
<AgentRunSummaryCard
|
||||
className={listItemClasses}
|
||||
@@ -199,7 +201,7 @@ export function AgentRunsSelectorList({
|
||||
?.name
|
||||
: null) ?? agent.name
|
||||
}
|
||||
timestamp={run.started_at}
|
||||
timestamp={run.started_at ?? undefined}
|
||||
selected={selectedView.id === run.id}
|
||||
onClick={() => onSelectRun(run.id)}
|
||||
onDelete={() => doDeleteRun(run as GraphExecutionMeta)}
|
||||
|
||||
@@ -120,9 +120,11 @@ export const AgentFlowList = ({
|
||||
lastRun =
|
||||
runCount == 0
|
||||
? null
|
||||
: _flowRuns.reduce((a, c) =>
|
||||
a.started_at > c.started_at ? a : c,
|
||||
);
|
||||
: _flowRuns.reduce((a, c) => {
|
||||
const aTime = a.started_at?.getTime() ?? 0;
|
||||
const cTime = c.started_at?.getTime() ?? 0;
|
||||
return aTime > cTime ? a : c;
|
||||
});
|
||||
}
|
||||
return { flow, runCount, lastRun };
|
||||
})
|
||||
@@ -130,10 +132,9 @@ export const AgentFlowList = ({
|
||||
if (!a.lastRun && !b.lastRun) return 0;
|
||||
if (!a.lastRun) return 1;
|
||||
if (!b.lastRun) return -1;
|
||||
return (
|
||||
b.lastRun.started_at.getTime() -
|
||||
a.lastRun.started_at.getTime()
|
||||
);
|
||||
const bTime = b.lastRun.started_at?.getTime() ?? 0;
|
||||
const aTime = a.lastRun.started_at?.getTime() ?? 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.map(({ flow, runCount, lastRun }) => (
|
||||
<TableRow
|
||||
|
||||
@@ -29,7 +29,10 @@ export const FlowRunsStatus: React.FC<{
|
||||
: statsSince;
|
||||
const filteredFlowRuns =
|
||||
statsSinceTimestamp != null
|
||||
? executions.filter((fr) => fr.started_at.getTime() > statsSinceTimestamp)
|
||||
? executions.filter(
|
||||
(fr) =>
|
||||
fr.started_at && fr.started_at.getTime() > statsSinceTimestamp,
|
||||
)
|
||||
: executions;
|
||||
|
||||
return (
|
||||
|
||||
@@ -98,40 +98,43 @@ export const FlowRunsTimeline = ({
|
||||
<Scatter
|
||||
key={flow.id}
|
||||
data={executions
|
||||
.filter((e) => e.graph_id == flow.graph_id)
|
||||
.filter((e) => e.graph_id == flow.graph_id && e.started_at)
|
||||
.map((e) => ({
|
||||
...e,
|
||||
time:
|
||||
e.started_at.getTime() + (e.stats?.node_exec_time ?? 0) * 1000,
|
||||
(e.started_at?.getTime() ?? 0) +
|
||||
(e.stats?.node_exec_time ?? 0) * 1000,
|
||||
_duration: e.stats?.node_exec_time ?? 0,
|
||||
}))}
|
||||
name={flow.name}
|
||||
fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`}
|
||||
/>
|
||||
))}
|
||||
{executions.map((execution) => (
|
||||
<Line
|
||||
key={execution.id}
|
||||
type="linear"
|
||||
dataKey="_duration"
|
||||
data={[
|
||||
{
|
||||
...execution,
|
||||
time: execution.started_at.getTime(),
|
||||
_duration: 0,
|
||||
},
|
||||
{
|
||||
...execution,
|
||||
time: execution.ended_at.getTime(),
|
||||
_duration: execution.stats?.node_exec_time ?? 0,
|
||||
},
|
||||
]}
|
||||
stroke={`hsl(${(hashString(execution.graph_id) * 137.5) % 360}, 70%, 50%)`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
legendType="none"
|
||||
/>
|
||||
))}
|
||||
{executions
|
||||
.filter((e) => e.started_at && e.ended_at)
|
||||
.map((execution) => (
|
||||
<Line
|
||||
key={execution.id}
|
||||
type="linear"
|
||||
dataKey="_duration"
|
||||
data={[
|
||||
{
|
||||
...execution,
|
||||
time: execution.started_at!.getTime(),
|
||||
_duration: 0,
|
||||
},
|
||||
{
|
||||
...execution,
|
||||
time: execution.ended_at!.getTime(),
|
||||
_duration: execution.stats?.node_exec_time ?? 0,
|
||||
},
|
||||
]}
|
||||
stroke={`hsl(${(hashString(execution.graph_id) * 137.5) % 360}, 70%, 50%)`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
legendType="none"
|
||||
/>
|
||||
))}
|
||||
<Legend
|
||||
content={<ScrollableLegend />}
|
||||
wrapperStyle={{
|
||||
|
||||
@@ -98,7 +98,11 @@ const Monitor = () => {
|
||||
...(selectedFlow
|
||||
? executions.filter((v) => v.graph_id == selectedFlow.graph_id)
|
||||
: executions),
|
||||
].sort((a, b) => b.started_at.getTime() - a.started_at.getTime())}
|
||||
].sort((a, b) => {
|
||||
const aTime = a.started_at?.getTime() ?? 0;
|
||||
const bTime = b.started_at?.getTime() ?? 0;
|
||||
return bTime - aTime;
|
||||
})}
|
||||
selectedRun={selectedRun}
|
||||
onSelectRun={(r) => setSelectedRun(r.id == selectedRun?.id ? null : r)}
|
||||
/>
|
||||
|
||||
@@ -6968,6 +6968,20 @@
|
||||
"error_message": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Error Message"
|
||||
},
|
||||
"started_at": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At"
|
||||
},
|
||||
"ended_at": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -7074,14 +7088,20 @@
|
||||
},
|
||||
"status": { "$ref": "#/components/schemas/AgentExecutionStatus" },
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Started At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At",
|
||||
"description": "When execution started running. Null if not yet started (QUEUED)."
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Ended At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At",
|
||||
"description": "When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW)."
|
||||
},
|
||||
"is_shared": {
|
||||
"type": "boolean",
|
||||
@@ -7115,8 +7135,6 @@
|
||||
"nodes_input_masks",
|
||||
"preset_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"stats",
|
||||
"outputs"
|
||||
],
|
||||
@@ -7215,14 +7233,20 @@
|
||||
},
|
||||
"status": { "$ref": "#/components/schemas/AgentExecutionStatus" },
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Started At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At",
|
||||
"description": "When execution started running. Null if not yet started (QUEUED)."
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Ended At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At",
|
||||
"description": "When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW)."
|
||||
},
|
||||
"is_shared": {
|
||||
"type": "boolean",
|
||||
@@ -7251,8 +7275,6 @@
|
||||
"nodes_input_masks",
|
||||
"preset_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"stats"
|
||||
],
|
||||
"title": "GraphExecutionMeta"
|
||||
@@ -7299,14 +7321,20 @@
|
||||
},
|
||||
"status": { "$ref": "#/components/schemas/AgentExecutionStatus" },
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Started At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At",
|
||||
"description": "When execution started running. Null if not yet started (QUEUED)."
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Ended At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At",
|
||||
"description": "When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW)."
|
||||
},
|
||||
"is_shared": {
|
||||
"type": "boolean",
|
||||
@@ -7345,8 +7373,6 @@
|
||||
"nodes_input_masks",
|
||||
"preset_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"stats",
|
||||
"outputs",
|
||||
"node_executions"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import React, { useCallback } from "react";
|
||||
import { GoogleDrivePicker } from "./GoogleDrivePicker";
|
||||
import { isValidFile } from "./helpers";
|
||||
|
||||
export interface Props {
|
||||
config: GoogleDrivePickerConfig;
|
||||
@@ -27,13 +28,15 @@ export function GoogleDrivePickerInput({
|
||||
const hasAutoCredentials = !!config.auto_credentials;
|
||||
|
||||
// Strip _credentials_id from value for display purposes
|
||||
const currentFiles = isMultiSelect
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: []
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
// Only show files section when there are valid file objects
|
||||
const currentFiles = React.useMemo(() => {
|
||||
if (isMultiSelect) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter(isValidFile);
|
||||
}
|
||||
if (!value || !isValidFile(value)) return [];
|
||||
return [value];
|
||||
}, [value, isMultiSelect]);
|
||||
|
||||
const handlePicked = useCallback(
|
||||
(files: any[], credentialId?: string) => {
|
||||
@@ -85,23 +88,27 @@ export function GoogleDrivePickerInput({
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
{/* Picker Button */}
|
||||
<GoogleDrivePicker
|
||||
multiselect={config.multiselect || false}
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={config.scopes || ["https://www.googleapis.com/auth/drive.file"]}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
{/* Picker Button */}
|
||||
<GoogleDrivePicker
|
||||
multiselect={config.multiselect || false}
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={
|
||||
config.scopes || ["https://www.googleapis.com/auth/drive.file"]
|
||||
}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Selected Files */}
|
||||
{currentFiles.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="mb-8 space-y-1">
|
||||
{currentFiles.map((file: any, idx: number) => (
|
||||
<div
|
||||
key={file.id || idx}
|
||||
|
||||
@@ -119,3 +119,14 @@ export function getCredentialsSchema(scopes: string[]) {
|
||||
secret: true,
|
||||
} satisfies BlockIOCredentialsSubSchema;
|
||||
}
|
||||
|
||||
export function isValidFile(
|
||||
file: unknown,
|
||||
): file is { id?: string; name?: string } {
|
||||
return (
|
||||
typeof file === "object" &&
|
||||
file !== null &&
|
||||
(typeof (file as { id?: unknown }).id === "string" ||
|
||||
typeof (file as { name?: unknown }).name === "string")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ export function ActivityItem({ execution }: Props) {
|
||||
execution.status === AgentExecutionStatus.QUEUED;
|
||||
|
||||
if (isActiveStatus) {
|
||||
const timeAgo = formatTimeAgo(execution.started_at.toString());
|
||||
const timeAgo = execution.started_at
|
||||
? formatTimeAgo(execution.started_at.toString())
|
||||
: "recently";
|
||||
const statusText =
|
||||
execution.status === AgentExecutionStatus.QUEUED ? "queued" : "running";
|
||||
return [
|
||||
@@ -61,7 +63,9 @@ export function ActivityItem({ execution }: Props) {
|
||||
// Handle all other statuses with time display
|
||||
const timeAgo = execution.ended_at
|
||||
? formatTimeAgo(execution.ended_at.toString())
|
||||
: formatTimeAgo(execution.started_at.toString());
|
||||
: execution.started_at
|
||||
? formatTimeAgo(execution.started_at.toString())
|
||||
: "recently";
|
||||
|
||||
let statusText = "ended";
|
||||
switch (execution.status) {
|
||||
|
||||
@@ -4,11 +4,10 @@ import {
|
||||
TemplatesType,
|
||||
} from "@rjsf/utils";
|
||||
import { AnyOfField } from "./anyof/AnyOfField";
|
||||
import { AddButton, CopyButton, RemoveButton } from "./standard/buttons";
|
||||
import {
|
||||
ArrayFieldItemTemplate,
|
||||
ArraySchemaField,
|
||||
ArrayFieldTemplate,
|
||||
ArraySchemaField,
|
||||
} from "./array";
|
||||
import {
|
||||
ObjectFieldTemplate,
|
||||
@@ -16,14 +15,16 @@ import {
|
||||
WrapIfAdditionalTemplate,
|
||||
} from "./object";
|
||||
import { DescriptionField, FieldTemplate, TitleField } from "./standard";
|
||||
import { AddButton, CopyButton, RemoveButton } from "./standard/buttons";
|
||||
import {
|
||||
TextWidget,
|
||||
SelectWidget,
|
||||
CheckboxWidget,
|
||||
FileWidget,
|
||||
DateWidget,
|
||||
TimeWidget,
|
||||
DateTimeWidget,
|
||||
DateWidget,
|
||||
FileWidget,
|
||||
GoogleDrivePickerWidget,
|
||||
SelectWidget,
|
||||
TextWidget,
|
||||
TimeWidget,
|
||||
} from "./standard/widgets";
|
||||
|
||||
const NoButton = () => null;
|
||||
@@ -65,5 +66,6 @@ export function generateBaseWidgets(): RegistryWidgetsType {
|
||||
DateWidget,
|
||||
TimeWidget,
|
||||
DateTimeWidget,
|
||||
GoogleDrivePickerWidget,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
|
||||
import { getFieldErrorKey } from "@/components/renderers/InputRenderer/utils/helpers";
|
||||
import type { GoogleDrivePickerConfig } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
|
||||
function hasGoogleDrivePickerConfig(
|
||||
schema: unknown,
|
||||
): schema is { google_drive_picker_config?: GoogleDrivePickerConfig } {
|
||||
return (
|
||||
typeof schema === "object" &&
|
||||
schema !== null &&
|
||||
"google_drive_picker_config" in schema
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleDrivePickerWidget(props: WidgetProps) {
|
||||
const { onChange, disabled, readonly, value, schema, id, formContext } =
|
||||
props;
|
||||
const { nodeId } = formContext || {};
|
||||
|
||||
const nodeErrors = useNodeStore((state) => {
|
||||
const node = state.nodes.find((n) => n.id === nodeId);
|
||||
return node?.data?.errors;
|
||||
});
|
||||
|
||||
const fieldErrorKey = getFieldErrorKey(id ?? "");
|
||||
const fieldError =
|
||||
nodeErrors?.[fieldErrorKey] ||
|
||||
nodeErrors?.[fieldErrorKey.replace(/_/g, ".")] ||
|
||||
nodeErrors?.[fieldErrorKey.replace(/\./g, "_")] ||
|
||||
undefined;
|
||||
|
||||
const config: GoogleDrivePickerConfig = hasGoogleDrivePickerConfig(schema)
|
||||
? schema.google_drive_picker_config || {}
|
||||
: {};
|
||||
|
||||
function handleChange(newValue: unknown) {
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<GoogleDrivePickerInput
|
||||
config={config}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
error={fieldError}
|
||||
className={cn(
|
||||
disabled || readonly ? "pointer-events-none opacity-50" : undefined,
|
||||
)}
|
||||
showRemoveButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GoogleDrivePickerWidget } from "./GoogleDrivePicketWidget";
|
||||
@@ -1,7 +1,8 @@
|
||||
export { default as TextWidget } from "./TextInput";
|
||||
export { SelectWidget } from "./SelectInput";
|
||||
export { CheckboxWidget } from "./CheckboxInput";
|
||||
export { FileWidget } from "./FileInput";
|
||||
export { DateWidget } from "./DateInput";
|
||||
export { TimeWidget } from "./TimeInput";
|
||||
export { DateTimeWidget } from "./DateTimeInput";
|
||||
export { FileWidget } from "./FileInput";
|
||||
export { GoogleDrivePickerWidget } from "./GoogleDrivePicker";
|
||||
export { SelectWidget } from "./SelectInput";
|
||||
export { default as TextWidget } from "./TextInput";
|
||||
export { TimeWidget } from "./TimeInput";
|
||||
|
||||
@@ -327,8 +327,8 @@ export type GraphExecutionMeta = {
|
||||
| "FAILED"
|
||||
| "INCOMPLETE"
|
||||
| "REVIEW";
|
||||
started_at: Date;
|
||||
ended_at: Date;
|
||||
started_at: Date | null;
|
||||
ended_at: Date | null;
|
||||
stats: {
|
||||
error: string | null;
|
||||
cost: number;
|
||||
|
||||
Reference in New Issue
Block a user