Download src/utils/automation-activity-log-export.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 5.57 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/utils/automation-activity-log-export.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/utils/automation-activity-log-export.ts
-
curl -L -o automation-activity-log-export.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/utils/automation-activity-log-export.ts
5.57 kB
| import type { | |
| ActivityLogExportFormat, | |
| Automation, | |
| AutomationRun, | |
| AutomationRunExportRow, | |
| } from "#/types/automation"; | |
| import { downloadBlob } from "#/utils/utils"; | |
| import AutomationService from "#/api/automation-service/automation-service.api"; | |
| const EXPORT_PAGE_SIZE = 100; | |
| const CSV_COLUMNS = [ | |
| "run_id", | |
| "automation_id", | |
| "automation_name", | |
| "trigger", | |
| "start_time", | |
| "end_time", | |
| "duration_seconds", | |
| "status", | |
| "conversation_id", | |
| "conversation_url", | |
| "error", | |
| // Appended rather than grouped with the other run metrics so existing | |
| // consumers that read the CSV by column position keep working. | |
| "cost", | |
| "phase", | |
| ] as const; | |
| export function getActivityLogExportFilename( | |
| automation: Pick<Automation, "id" | "name">, | |
| format: ActivityLogExportFormat, | |
| ): string { | |
| const slug = automation.name | |
| .trim() | |
| .toLowerCase() | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, ""); | |
| return `${slug || automation.id}.activity-log.${format}`; | |
| } | |
| function csvEscape(value: string): string { | |
| if (/[",\n\r]/.test(value)) { | |
| return `"${value.replace(/"/g, '""')}"`; | |
| } | |
| return value; | |
| } | |
| export function serializeActivityLogRowsCsv( | |
| rows: AutomationRunExportRow[], | |
| ): string { | |
| const lines: string[] = [CSV_COLUMNS.join(",")]; | |
| for (const row of rows) { | |
| const values = CSV_COLUMNS.map((key) => { | |
| const raw = row[key]; | |
| if (raw == null) return ""; | |
| if (key === "trigger") { | |
| return csvEscape(JSON.stringify(raw)); | |
| } | |
| return csvEscape(String(raw)); | |
| }); | |
| lines.push(values.join(",")); | |
| } | |
| return `${lines.join("\n")}\n`; | |
| } | |
| export function buildConversationUrl( | |
| conversationId: string | null, | |
| conversationBaseUrl?: string, | |
| ): string | null { | |
| if (!conversationId) return null; | |
| const path = `/conversations/${conversationId}`; | |
| if (!conversationBaseUrl) return path; | |
| return `${conversationBaseUrl.replace(/\/$/, "")}${path}`; | |
| } | |
| export function mapAutomationRunToExportRow( | |
| run: AutomationRun, | |
| automation: Pick<Automation, "id" | "name" | "trigger">, | |
| conversationBaseUrl?: string, | |
| ): AutomationRunExportRow { | |
| const start = run.started_at || null; | |
| const end = run.completed_at; | |
| let duration_seconds: number | null = null; | |
| if (start && end) { | |
| duration_seconds = | |
| (new Date(end).getTime() - new Date(start).getTime()) / 1000; | |
| } | |
| return { | |
| run_id: run.id, | |
| automation_id: automation.id, | |
| automation_name: automation.name, | |
| trigger: automation.trigger, | |
| start_time: start, | |
| end_time: end, | |
| duration_seconds, | |
| status: run.status, | |
| conversation_id: run.conversation_id, | |
| conversation_url: buildConversationUrl( | |
| run.conversation_id, | |
| conversationBaseUrl, | |
| ), | |
| error: run.error_detail, | |
| // Exports carry the raw number and leave formatting to the consumer; the | |
| // Activity Log row is the only place that renders it as USD. | |
| cost: run.cost ?? null, | |
| // Deliberately not filtered through `shouldShowRunPhase`: that predicate | |
| // decides what is worth a user's attention on screen, and hides the phase | |
| // of a run whose status already says everything. An export is a record, | |
| // not a screen — a completed run's last phase is real data, and dropping | |
| // it here would make the phase of a run that finished indistinguishable | |
| // from one that never reported a phase at all. | |
| // | |
| // Falling back to the label: without it, a phase reported with only a | |
| // label would export as an empty cell, indistinguishable from no phase. | |
| // Trimmed rather than nullish-coalesced: the service stores a | |
| // whitespace-only field as sent — it rejects only a phase blank on | |
| // *both* — so `??` would let a blank code suppress a real label, which | |
| // is exactly the empty cell this fallback exists to prevent. | |
| phase: run.phase_code?.trim() || run.phase_label?.trim() || null, | |
| }; | |
| } | |
| /** | |
| * Page ``GET /v1/{id}/runs`` until complete, then project export rows locally. | |
| */ | |
| export async function fetchAllActivityLogExportRows( | |
| automation: Pick<Automation, "id" | "name" | "trigger">, | |
| conversationBaseUrl?: string, | |
| ): Promise<AutomationRunExportRow[]> { | |
| const rows: AutomationRunExportRow[] = []; | |
| let offset = 0; | |
| let total = Number.POSITIVE_INFINITY; | |
| while (offset < total) { | |
| const page = await AutomationService.listAutomationRuns(automation.id, { | |
| limit: EXPORT_PAGE_SIZE, | |
| offset, | |
| }); | |
| for (const run of page.runs) { | |
| rows.push( | |
| mapAutomationRunToExportRow(run, automation, conversationBaseUrl), | |
| ); | |
| } | |
| total = page.total; | |
| offset += page.runs.length; | |
| if (page.runs.length === 0) break; | |
| } | |
| return rows; | |
| } | |
| /** | |
| * Page the runs list endpoint and download one CSV or JSON file. | |
| */ | |
| export async function downloadActivityLogExport(options: { | |
| automation: Pick<Automation, "id" | "name" | "trigger">; | |
| format: ActivityLogExportFormat; | |
| conversationBaseUrl?: string; | |
| }): Promise<void> { | |
| const { automation, format, conversationBaseUrl } = options; | |
| const rows = await fetchAllActivityLogExportRows( | |
| automation, | |
| conversationBaseUrl, | |
| ); | |
| if (format === "json") { | |
| downloadBlob( | |
| new Blob( | |
| [`${JSON.stringify({ runs: rows, total: rows.length }, null, 2)}\n`], | |
| { type: "application/json" }, | |
| ), | |
| getActivityLogExportFilename(automation, "json"), | |
| ); | |
| return; | |
| } | |
| downloadBlob( | |
| new Blob([serializeActivityLogRowsCsv(rows)], { | |
| type: "text/csv;charset=utf-8", | |
| }), | |
| getActivityLogExportFilename(automation, "csv"), | |
| ); | |
| } | |