Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions app/src/features/account/TokensByBot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Tokens each bot used over a period (spec 8.7), in the usage dialog: the
// new text read plus the replies written, with the conversation reread
// each time shown apart.
// new text read plus the replies written, with the conversation sent
// again after a pause shown apart. Each bot shows its crew, as two crews can have
// bots with the same name.

import { useEffect, useState } from "react";
import { useT } from "../../i18n";
Expand Down Expand Up @@ -99,7 +100,7 @@ function Row({ bot, most }: { bot: BotTokens; most: number }) {
/>
</div>
<span className="text-muted text-xs">
{u.detail(bot.turns, tokens(bot.tokens.cacheRead), reloadedTokens(bot.tokens.reloaded))}
{u.detail(bot.crew, bot.turns, reloadedTokens(bot.tokens.reloaded))}
</span>
</div>
</li>
Expand All @@ -110,17 +111,14 @@ function Total({ bots }: { bots: BotTokens[] }) {
const u = useT().account.usage.tokens;
const used = bots.reduce((sum, bot) => sum + usedTokens(bot.tokens), 0);
const turns = bots.reduce((sum, bot) => sum + bot.turns, 0);
const reread = bots.reduce((sum, bot) => sum + bot.tokens.cacheRead, 0);
const reloaded = bots.reduce((sum, bot) => sum + bot.tokens.reloaded, 0);
return (
<li className="flex flex-col gap-1 border-line border-t pt-3">
<div className="flex items-baseline justify-between gap-3 font-semibold">
<span>{u.total}</span>
<span className="tabular-nums">{tokens(used)}</span>
</div>
<span className="text-muted text-xs">
{u.detail(turns, tokens(reread), reloadedTokens(reloaded))}
</span>
<span className="text-muted text-xs">{u.detail(null, turns, reloadedTokens(reloaded))}</span>
</li>
);
}
30 changes: 25 additions & 5 deletions app/src/features/account/tokens.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ describe("tokens by bot", () => {
const rows = within(list).getAllByRole("listitem");
// Each turn: 12 new, 1,800 into the cache and 640 written count; 24,000 reread do not.
expect(rows.map((row) => row.textContent)).toEqual([
"Scout4.9kWorked 2 times · 48k reread",
"Writer2.5kWorked once · 24k reread",
"All bots7.4kWorked 3 times · 72k reread",
"Scout4.9kOps · Worked 2 times",
"Writer2.5kOps · Worked once",
"All bots7.4kWorked 3 times",
]);

fireEvent.click(within(dialog).getByRole("radio", { name: "7 days" }));
expect(await within(dialog).findByText("4.9k")).toBeDefined();
expect(dialog.textContent).toContain("Writer4.9kWorked 2 times");
expect(dialog.textContent).toContain("Writer4.9kOps · Worked 2 times");
expect(dialog.textContent).toContain("All bots9.8k");
});

Expand All @@ -64,7 +64,27 @@ describe("tokens by bot", () => {
within(list)
.getAllByRole("listitem")
.map((row) => row.textContent),
).toEqual(["Scout4.1kWorked 2 times · 24k reread · 72k reloaded"]);
).toEqual(["Scout4.1kOps · Worked 2 times · 72k reloaded"]);
});

test("bots with the same name in two crews show their crew", async () => {
const fake = new FakeBotloft();
const ops = fake.addCrew("Ops");
const news = fake.addCrew("Newsroom");
fake.chat.turn(fake.addBot(ops.id, "Scout", "Finds sources").id);
fake.chat.turn(fake.addBot(news.id, "Scout", "Finds stories").id);

const dialog = await openUsage(fake);
const list = await within(dialog).findByRole("list", { name: "Tokens by bot" });
expect(
within(list)
.getAllByRole("listitem")
.map((row) => row.textContent),
).toEqual([
"Scout2.5kNewsroom · Worked once",
"Scout2.5kOps · Worked once",
"All bots4.9kWorked 2 times",
]);
});

test("a period with no work says so", async () => {
Expand Down
1 change: 0 additions & 1 deletion app/src/features/chat/BotRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ function TurnEnd({ turn }: { turn: TurnItem }) {
const detail = used && {
read: tokens(used.input + used.cacheWrite - used.reloaded),
wrote: tokens(used.output),
reread: tokens(used.cacheRead),
reloaded,
};
return (
Expand Down
4 changes: 1 addition & 3 deletions app/src/features/chat/chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ describe("chat", () => {
expect(within(chat()).getByText("Command")).toBeDefined();
expect(within(chat()).getByText("npm test")).toBeDefined();
const done = within(chat()).getByText("Done in 4.2 s · 2.5k tokens");
expect(done.title).toBe(
"Took 4.2 s. Read 1.8k new tokens and wrote 640; reread 24k of the conversation, which weighs much less.",
);
expect(done.title).toBe("Took 4.2 s. Read 1.8k new tokens and wrote 640.");
});

test("an approval waits for the owner, and a denial carries a note", async () => {
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/chat/context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("conversation space", () => {
});
const done = screen.getByText("Done in 4.2 s · 1.6k tokens · reloaded the conversation (72k)");
expect(done.title).toBe(
"Took 4.2 s. Read 638 new tokens and wrote 983; reread 0 of the conversation, which weighs much less. It also sent 72k of the conversation again, as the model no longer had it after a pause.",
"Took 4.2 s. Read 638 new tokens and wrote 983. It also sent 72k of the conversation again, as the model no longer had it after a pause.",
);
});

Expand Down
7 changes: 4 additions & 3 deletions app/src/i18n/en/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export const account = {
tokens: {
title: "Tokens by bot",
intro:
"Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written. The conversation reread each time shows apart, as it weighs much less, and so does the conversation sent again after a pause.",
"Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written. The conversation sent again after a pause shows apart.",
period: "Period",
periods: { today: "Today", week: "7 days", month: "30 days", all: "All time" },
empty: "No bot worked in this period.",
failed: "Could not load the tokens",
archived: "archived",
detail: (times: number, reread: string, reloaded: string | null) =>
`Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread${
/** `crew` is null on the total of all bots. */
detail: (crew: string | null, times: number, reloaded: string | null) =>
`${crew === null ? "" : `${crew} · `}Worked ${times === 1 ? "once" : `${times} times`}${
reloaded === null ? "" : ` · ${reloaded} reloaded`
}`,
total: "All bots",
Expand Down
3 changes: 1 addition & 2 deletions app/src/i18n/en/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
export interface TurnTokens {
read: string;
wrote: string;
reread: string;
/** The conversation sent again after a pause, if it was worth telling. */
reloaded: string | null;
}
Expand Down Expand Up @@ -64,7 +63,7 @@ export const chat = {
`Took ${time}${
tokens === null
? ""
: `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}; reread ${tokens.reread} of the conversation, which weighs much less.${
: `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}.${
tokens.reloaded === null
? ""
: ` It also sent ${tokens.reloaded} of the conversation again, as the model no longer had it after a pause.`
Expand Down
7 changes: 4 additions & 3 deletions app/src/i18n/es/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
"Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas. La conversación releída cada vez aparece aparte, porque pesa mucho menos, y también la conversación enviada de nuevo tras una pausa.",
"Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas. La conversación enviada de nuevo tras una pausa aparece aparte.",
period: "Período",
periods: { today: "Hoy", week: "7 días", month: "30 días", all: "Todo" },
empty: "Ningún bot trabajó en este período.",
failed: "No se pudieron cargar los tokens",
archived: "archivado",
detail: (times: number, reread: string, reloaded: string | null) =>
`Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos${
/** `crew` is null on the total of all bots. */
detail: (crew: string | null, times: number, reloaded: string | null) =>
`${crew === null ? "" : `${crew} · `}Trabajó ${times === 1 ? "1 vez" : `${times} veces`}${
reloaded === null ? "" : ` · ${reloaded} recargados`
}`,
total: "Todos los bots",
Expand Down
2 changes: 1 addition & 1 deletion app/src/i18n/es/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const chat: Messages["chat"] = {
`Tardó ${time}${
tokens === null
? ""
: `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}; releyó ${tokens.reread} de la conversación, que pesa mucho menos.${
: `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}.${
tokens.reloaded === null
? ""
: ` También volvió a enviar ${tokens.reloaded} de la conversación, que el modelo ya no tenía tras una pausa.`
Expand Down
7 changes: 4 additions & 3 deletions app/src/i18n/pt-BR/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
"Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas. A conversa relida a cada vez aparece à parte, porque pesa bem menos, e a conversa mandada de novo depois de uma pausa também.",
"Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas. A conversa mandada de novo depois de uma pausa aparece à parte.",
period: "Período",
periods: { today: "Hoje", week: "7 dias", month: "30 dias", all: "Tudo" },
empty: "Nenhum bot trabalhou nesse período.",
failed: "Não deu para carregar os tokens",
archived: "arquivado",
detail: (times: number, reread: string, reloaded: string | null) =>
`Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos${
/** `crew` is null on the total of all bots. */
detail: (crew: string | null, times: number, reloaded: string | null) =>
`${crew === null ? "" : `${crew} · `}Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`}${
reloaded === null ? "" : ` · ${reloaded} recarregados`
}`,
total: "Todos os bots",
Expand Down
2 changes: 1 addition & 1 deletion app/src/i18n/pt-BR/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const chat: Messages["chat"] = {
`Levou ${time}${
tokens === null
? ""
: `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}; releu ${tokens.reread} da conversa, que pesa bem menos.${
: `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}.${
tokens.reloaded === null
? ""
: ` Também mandou de novo ${tokens.reloaded} da conversa, que o modelo já não tinha depois de uma pausa.`
Expand Down
6 changes: 5 additions & 1 deletion app/src/lib/fakeUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function usageHandlers(fake: FakeBotloft): Pick<Handlers, "usage.tokens">
botId: bot.id,
name: bot.name,
color: bot.color,
crew: fake.crew(bot.crewId, false).name,
archived: bot.archivedAt !== null,
turns: 0,
tokens: { input: 0, cacheWrite: 0, reloaded: 0, cacheRead: 0, output: 0 },
Expand All @@ -37,7 +38,10 @@ export function usageHandlers(fake: FakeBotloft): Pick<Handlers, "usage.tokens">
};
}
return [...byBot.values()].sort(
(a, b) => usedTokens(b.tokens) - usedTokens(a.tokens) || a.name.localeCompare(b.name),
(a, b) =>
usedTokens(b.tokens) - usedTokens(a.tokens) ||
a.name.localeCompare(b.name) ||
a.crew.localeCompare(b.crew),
);
},
};
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ export function tokens(count: number): string {
/**
* The tokens a turn counts for (spec 8.7): new input read, with what went
* into the prompt cache, and what the model wrote. The conversation reread
* from the cache weighs much less and is shown apart, and so is the
* conversation written to the cache again after it expired.
* from the cache is left out, and the conversation written to the cache
* again after it expired is shown apart.
*/
export function usedTokens(usage: TokenUsage): number {
return usage.input + usage.cacheWrite - usage.reloaded + usage.output;
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/protocol.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,10 @@ since: number, };
* One bot's tokens since the time asked for.
*/
export type BotTokens = { botId: BotId, name: string, color: string,
/**
* The name of the bot's crew, so two bots with one name stay apart.
*/
crew: string,
/**
* The bot is archived.
*/
Expand Down
2 changes: 2 additions & 0 deletions crates/botloft-core/src/protocol/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct BotTokens {
pub bot_id: BotId,
pub name: String,
pub color: String,
/// The name of the bot's crew, so two bots with one name stay apart.
pub crew: String,
/// The bot is archived.
pub archived: bool,
/// Turns that reported tokens.
Expand Down
9 changes: 6 additions & 3 deletions crates/botloft-store/src/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,22 @@ impl Store {
SUM(json_extract(c.data, '$.tokens.cacheWrite')), \
SUM(json_extract(c.data, '$.tokens.cacheRead')), \
SUM(json_extract(c.data, '$.tokens.output')) AS output, \
COALESCE(SUM(json_extract(c.data, '$.tokens.reloaded')), 0) AS reloaded \
FROM chat_items c JOIN bots b ON b.id = c.bot_id \
COALESCE(SUM(json_extract(c.data, '$.tokens.reloaded')), 0) AS reloaded, \
w.name \
FROM chat_items c JOIN bots b ON b.id = c.bot_id JOIN crews w ON w.id = b.crew_id \
WHERE c.kind = 'turn' AND c.created_at >= ?1 \
AND json_type(c.data, '$.tokens') = 'object' \
GROUP BY b.id \
ORDER BY SUM(json_extract(c.data, '$.tokens.input')) \
+ SUM(json_extract(c.data, '$.tokens.cacheWrite')) - reloaded + output DESC, \
b.name",
b.name, w.name",
)?;
let rows = stmt.query_map(params![since], |row| {
Ok(BotTokens {
bot_id: parse_column(row, 0)?,
name: row.get(1)?,
color: row.get(2)?,
crew: row.get(10)?,
archived: row.get(3)?,
turns: row.get(4)?,
tokens: TokenUsage {
Expand Down Expand Up @@ -104,6 +106,7 @@ mod tests {
assert_eq!(order, [writer, scout], "the bot that used the most first");
let scout_tokens = &all[1];
assert_eq!(scout_tokens.name, "scout");
assert_eq!(scout_tokens.crew, "Ops");
assert!(!scout_tokens.archived);
assert_eq!(scout_tokens.turns, 2);
assert_eq!(
Expand Down
Loading
Loading