@f5-sales-demo/xcsh-stats 19.105.7 → 20.0.0
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 +4 -3
- package/src/aggregator.ts +6 -9
- package/src/parser.ts +38 -0
- package/src/types.ts +12 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh-stats",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "20.0.0",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
"check": "biome check . && bun run check:types",
|
|
34
34
|
"check:types": "tsgo -p tsconfig.json --noEmit && tsgo -p tsconfig.client.json --noEmit",
|
|
35
35
|
"lint": "biome lint .",
|
|
36
|
+
"test": "bun test",
|
|
36
37
|
"fix": "biome check --write --unsafe .",
|
|
37
38
|
"fmt": "biome format --write ."
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@f5-sales-demo/pi-ai": "
|
|
41
|
-
"@f5-sales-demo/pi-utils": "
|
|
41
|
+
"@f5-sales-demo/pi-ai": "20.0.0",
|
|
42
|
+
"@f5-sales-demo/pi-utils": "20.0.0",
|
|
42
43
|
"@tailwindcss/node": "^4.2",
|
|
43
44
|
"chart.js": "^4.5",
|
|
44
45
|
"date-fns": "~4.4.0",
|
package/src/aggregator.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
insertMessageStats,
|
|
16
16
|
setFileOffset,
|
|
17
17
|
} from "./db";
|
|
18
|
-
import {
|
|
18
|
+
import { getSessionMessageChain, listAllSessionFiles, parseSessionFile } from "./parser";
|
|
19
19
|
import type { DashboardStats, MessageStats, RequestDetails } from "./types";
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -106,17 +106,14 @@ export async function getRequestDetails(id: number): Promise<RequestDetails | nu
|
|
|
106
106
|
const msg = getMessageById(id);
|
|
107
107
|
if (!msg) return null;
|
|
108
108
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// TODO: Get parent/context messages?
|
|
113
|
-
// For now we return the single entry which contains the assistant response.
|
|
114
|
-
// The user prompt is likely the parent.
|
|
109
|
+
const entries = await getSessionMessageChain(msg.sessionFile, msg.entryId);
|
|
110
|
+
const entry = entries.at(-1);
|
|
111
|
+
if (entry?.message.role !== "assistant") return null;
|
|
115
112
|
|
|
116
113
|
return {
|
|
117
114
|
...msg,
|
|
118
|
-
messages:
|
|
119
|
-
output:
|
|
115
|
+
messages: entries.map(item => item.message),
|
|
116
|
+
output: entry.message,
|
|
120
117
|
};
|
|
121
118
|
}
|
|
122
119
|
|
package/src/parser.ts
CHANGED
|
@@ -167,3 +167,41 @@ export async function getSessionEntry(sessionPath: string, entryId: string): Pro
|
|
|
167
167
|
}
|
|
168
168
|
return null;
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reconstruct the message ancestry ending at a session entry.
|
|
173
|
+
*
|
|
174
|
+
* Non-message tree entries are traversed but omitted from the returned conversation.
|
|
175
|
+
* Missing parents and malformed cycles terminate the walk without hiding the messages
|
|
176
|
+
* that were recovered successfully.
|
|
177
|
+
*/
|
|
178
|
+
export async function getSessionMessageChain(sessionPath: string, entryId: string): Promise<SessionMessageEntry[]> {
|
|
179
|
+
let bytes: Uint8Array;
|
|
180
|
+
try {
|
|
181
|
+
bytes = await Bun.file(sessionPath).bytes();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (isEnoent(err)) return [];
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const { entries } = parseSessionEntriesLenient(bytes);
|
|
188
|
+
const byId = new Map<string, SessionMessageEntry | (SessionEntry & { id: string; parentId: string | null })>();
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if ("id" in entry && "parentId" in entry && typeof entry.id === "string") {
|
|
191
|
+
byId.set(entry.id, entry);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const messages: SessionMessageEntry[] = [];
|
|
196
|
+
const visited = new Set<string>();
|
|
197
|
+
let current = byId.get(entryId);
|
|
198
|
+
while (current && !visited.has(current.id)) {
|
|
199
|
+
visited.add(current.id);
|
|
200
|
+
if (current.type === "message" && "message" in current) {
|
|
201
|
+
messages.push(current as SessionMessageEntry);
|
|
202
|
+
}
|
|
203
|
+
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return messages.reverse();
|
|
207
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantMessage, StopReason, Usage } from "@f5-sales-demo/pi-ai";
|
|
1
|
+
import type { AssistantMessage, Message, StopReason, Usage } from "@f5-sales-demo/pi-ai";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracted stats from an assistant message.
|
|
@@ -36,8 +36,8 @@ export interface MessageStats {
|
|
|
36
36
|
* Full details of a request, including content.
|
|
37
37
|
*/
|
|
38
38
|
export interface RequestDetails extends MessageStats {
|
|
39
|
-
messages:
|
|
40
|
-
output:
|
|
39
|
+
messages: Message[];
|
|
40
|
+
output: AssistantMessage;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -169,7 +169,14 @@ export interface SessionMessageEntry {
|
|
|
169
169
|
id: string;
|
|
170
170
|
parentId: string | null;
|
|
171
171
|
timestamp: string;
|
|
172
|
-
message:
|
|
172
|
+
message: Message;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
export
|
|
175
|
+
export interface SessionTreeEntry {
|
|
176
|
+
type: string;
|
|
177
|
+
id: string;
|
|
178
|
+
parentId: string | null;
|
|
179
|
+
[key: string]: unknown;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type SessionEntry = SessionHeader | SessionMessageEntry | SessionTreeEntry | { type: string };
|