@a-t-h-i/bot-lobby 0.6.31 → 0.6.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/webui/api/index.ts +2 -2
- package/src/webui/api/metrics.ts +41 -8
- package/src/webui/protocol.ts +2 -1
- package/webui/dist/assets/index-BFra71Rf.js +108 -0
- package/webui/dist/assets/index-D5_tWSK0.css +2 -0
- package/webui/dist/build.json +55 -27
- package/webui/dist/index.html +2 -2
- package/webui/dist/licenses/obsidian.txt +21 -0
- package/webui/dist/licenses/reui.txt +9 -0
- package/webui/dist/assets/index-C9o7wp-z.css +0 -2
- package/webui/dist/assets/index-R54L2ZET.js +0 -108
package/package.json
CHANGED
package/src/webui/api/index.ts
CHANGED
|
@@ -271,8 +271,8 @@ function buildRoutes(): Record<string, Route> {
|
|
|
271
271
|
run: (body, ctx) => quickfixMovedToTask(body as { id: string }, ctx),
|
|
272
272
|
};
|
|
273
273
|
routes["metrics.get"] = {
|
|
274
|
-
schema: Type.Object({ groupBy: Type.Union([Type.Literal("model"), Type.Literal("model-kind")]), query: Type.Optional(Type.String({ maxLength: 500 })) }, { additionalProperties: false }),
|
|
275
|
-
run: (body, ctx) => metricsGet(body as
|
|
274
|
+
schema: Type.Object({ groupBy: Type.Union([Type.Literal("model"), Type.Literal("model-kind")]), query: Type.Optional(Type.String({ maxLength: 500 })), from: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })), to: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })), timeZone: Type.Optional(Type.String({ maxLength: 100 })) }, { additionalProperties: false }),
|
|
275
|
+
run: (body, ctx) => metricsGet(body as Parameters<typeof metricsGet>[0], ctx),
|
|
276
276
|
};
|
|
277
277
|
routes["knowledge.files"] = { schema: Empty, run: (_body, ctx) => knowledgeFiles(ctx) };
|
|
278
278
|
routes["knowledge.ask"] = {
|
package/src/webui/api/metrics.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* time goes, and what the classifier did — the same figures the terminal
|
|
4
4
|
* tab draws, as data. Search narrows runs exactly like the tab's `/`.
|
|
5
5
|
*/
|
|
6
|
-
import { aggregateMetrics, collectMetrics, sortGroups, summarizeClassifier, taskStats, taskTimesByModel, type
|
|
6
|
+
import { aggregateMetrics, collectMetrics, sortGroups, summarizeClassifier, taskStats, taskTimesByModel, type MetricRecord } from "../../state/metrics.ts";
|
|
7
7
|
import { filterRecords } from "../../lobby/models/metrics.ts";
|
|
8
|
-
import type { MetricsData } from "../protocol.ts";
|
|
9
|
-
import type
|
|
8
|
+
import type { Api, MetricsData } from "../protocol.ts";
|
|
9
|
+
import { fail, type ApiContext } from "./index.ts";
|
|
10
10
|
|
|
11
11
|
function timed(records: readonly MetricRecord[]): MetricRecord[] {
|
|
12
12
|
return records.filter((record) => record.status !== "cancelled" && record.durationMs > 0);
|
|
@@ -49,15 +49,48 @@ function shareOf(records: readonly MetricRecord[]): MetricsData["timeShare"]["by
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** The dashboard figures for one grouping and search. */
|
|
52
|
-
export function metricsGet(body:
|
|
53
|
-
const
|
|
54
|
-
const
|
|
52
|
+
export function metricsGet(body: Api["metrics.get"]["request"], ctx: ApiContext): MetricsData {
|
|
53
|
+
const { from, to } = body;
|
|
54
|
+
const validDate = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
55
|
+
if ((from === undefined) !== (to === undefined) || (from !== undefined && (!validDate(from) || !validDate(to!) || from > to!))) {
|
|
56
|
+
fail(400, "bad_request", "Choose a valid start and end date, with the start on or before the end.");
|
|
57
|
+
}
|
|
58
|
+
let formatter: Intl.DateTimeFormat;
|
|
59
|
+
try {
|
|
60
|
+
formatter = new Intl.DateTimeFormat("en", { timeZone: body.timeZone ?? "UTC", year: "numeric", month: "2-digit", day: "2-digit" });
|
|
61
|
+
} catch {
|
|
62
|
+
fail(400, "bad_request", "Choose a valid time zone.");
|
|
63
|
+
}
|
|
64
|
+
const dayOf = (timestamp: string) => {
|
|
65
|
+
const date = new Date(timestamp);
|
|
66
|
+
if (!Number.isFinite(date.getTime())) return "";
|
|
67
|
+
const parts = formatter.formatToParts(date);
|
|
68
|
+
return ["year", "month", "day"].map((part) => parts.find((item) => item.type === part)!.value).join("-");
|
|
69
|
+
};
|
|
70
|
+
const inRange = (timestamp: string) => !from || (dayOf(timestamp) >= from && dayOf(timestamp) <= to!);
|
|
71
|
+
const allTasks = ctx.service.tasks();
|
|
72
|
+
const tasks = allTasks.filter((task) => inRange(task.state === "completed" ? task.updatedAt : task.createdAt));
|
|
73
|
+
const allRecords = filterRecords(collectMetrics(ctx.service.metrics(), allTasks), body.query);
|
|
74
|
+
const records = allRecords.filter((record) => inRange(record.startedAt));
|
|
55
75
|
const stats = taskStats(tasks);
|
|
56
|
-
const classifier = summarizeClassifier(ctx.service.classifierMetrics?.() ?? [], records);
|
|
76
|
+
const classifier = summarizeClassifier((ctx.service.classifierMetrics?.() ?? []).filter((record) => inRange(record.startedAt)), records);
|
|
77
|
+
const daily = new Map<string, MetricsData["daily"][number]>();
|
|
78
|
+
for (const record of records) {
|
|
79
|
+
const date = dayOf(record.startedAt);
|
|
80
|
+
if (!date) continue;
|
|
81
|
+
const entry = daily.get(date) ?? { date, runs: 0, successes: 0, cost: 0 };
|
|
82
|
+
entry.runs += 1;
|
|
83
|
+
entry.successes += Number(record.status === "success");
|
|
84
|
+
entry.cost += record.cost ?? 0;
|
|
85
|
+
daily.set(date, entry);
|
|
86
|
+
}
|
|
87
|
+
// Include range edges without allocating an entry for every empty day.
|
|
88
|
+
for (const date of [from, to]) if (date && !daily.has(date)) daily.set(date, { date, runs: 0, successes: 0, cost: 0 });
|
|
57
89
|
return {
|
|
58
90
|
tiles: tilesOf(records, stats),
|
|
91
|
+
daily: [...daily.values()].sort((a, b) => a.date.localeCompare(b.date)),
|
|
59
92
|
groups: sortGroups(aggregateMetrics(records, body.groupBy), "runs"),
|
|
60
|
-
timeShare: { byAgent: shareOf(records), taskTimes: taskTimesByModel(tasks,
|
|
93
|
+
timeShare: { byAgent: shareOf(records), taskTimes: taskTimesByModel(tasks, allRecords) },
|
|
61
94
|
...(classifier ? { classifier } : {}),
|
|
62
95
|
};
|
|
63
96
|
}
|
package/src/webui/protocol.ts
CHANGED
|
@@ -335,6 +335,7 @@ export interface MetricsClassifier {
|
|
|
335
335
|
export interface MetricsData {
|
|
336
336
|
tiles: MetricsTiles;
|
|
337
337
|
groups: MetricGroupInfo[];
|
|
338
|
+
daily: Array<{ date: string; runs: number; successes: number; cost: number }>;
|
|
338
339
|
timeShare: { byAgent: MetricsAgentShare[]; taskTimes: MetricsTaskTime[] };
|
|
339
340
|
classifier?: MetricsClassifier;
|
|
340
341
|
}
|
|
@@ -634,7 +635,7 @@ export interface Api {
|
|
|
634
635
|
"quickfix.cancel": { request: { id: string }; result: { notice: string } };
|
|
635
636
|
"quickfix.runAnyway": { request: { id: string }; result: { notice: string } };
|
|
636
637
|
"quickfix.movedToTask": { request: { id: string }; result: { notice: string; key?: string } };
|
|
637
|
-
"metrics.get": { request: { groupBy: "model" | "model-kind"; query?: string }; result: MetricsData };
|
|
638
|
+
"metrics.get": { request: { groupBy: "model" | "model-kind"; query?: string; from?: string; to?: string; timeZone?: string }; result: MetricsData };
|
|
638
639
|
"projects.browse": { request: { path?: string }; result: FolderListing };
|
|
639
640
|
"projects.open": { request: { path: string }; result: { project: ProjectInfo } };
|
|
640
641
|
"knowledge.files": { request: Record<string, never>; result: { files: KnowledgeFileInfo[]; models?: Partial<Record<KnowledgeAgentName, string>> } };
|