Skip to content

Commit ed7e112

Browse files
authored
Merge pull request #204 from httpsphl/feat/bot-catalog
feat(catalog): twelve bot role sheets and catalog.list/get/add (G1)
2 parents 7a5ca91 + c923fc5 commit ed7e112

29 files changed

Lines changed: 1139 additions & 4 deletions

‎app/src/lib/fake.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FakeAllow } from "./fakeAllow";
77
import { backupHandlers } from "./fakeBackup";
88
import { botHandlers } from "./fakeBots";
99
import { FakeBrowser } from "./fakeBrowser";
10+
import { catalogHandlers } from "./fakeCatalog";
1011
import { FakeChat } from "./fakeChat";
1112
import { FakeConversation } from "./fakeConversation";
1213
import { FakeCrewAccess } from "./fakeCrewAccess";
@@ -272,6 +273,7 @@ export class FakeBotloft implements BotloftApi {
272273
}),
273274
...crewHandlers(this),
274275
...botHandlers(this),
276+
...catalogHandlers(this),
275277
...this.chat.handlers(),
276278
...this.allow.handlers(),
277279
...this.crewAccess.handlers(),

‎app/src/lib/fakeCatalog.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from "vitest";
2+
import { FakeBotloft } from "./fake";
3+
4+
describe("the fake's bot catalog", () => {
5+
it("lists the roles, by category too, and reads one with its instructions", async () => {
6+
const fake = new FakeBotloft();
7+
expect(await fake.call("catalog.list", {})).toHaveLength(12);
8+
const code = await fake.call("catalog.list", { category: "code" });
9+
expect(code.map((role) => role.id)).toEqual(["developer", "code-reviewer", "qa-tester"]);
10+
expect(code[0]).not.toHaveProperty("instructions");
11+
12+
const reviewer = await fake.call("catalog.get", { id: "code-reviewer" });
13+
expect(reviewer.effort).toBe("high");
14+
expect(reviewer.instructions).toContain("### What you do");
15+
await expect(fake.call("catalog.get", { id: "wizard" })).rejects.toThrow();
16+
});
17+
18+
it("adds a role as a plain bot, with the name and role the app wrote", async () => {
19+
const fake = new FakeBotloft();
20+
const crew = fake.addCrew("Studio");
21+
const seen: string[] = [];
22+
fake.subscribe((event) => seen.push(event.name));
23+
24+
const bot = await fake.call("catalog.add", {
25+
crewId: crew.id,
26+
templateId: "code-reviewer",
27+
name: "Revisor de código",
28+
role: "Revisa o que os outros bots mudaram",
29+
});
30+
const sheet = await fake.call("catalog.get", { id: "code-reviewer" });
31+
expect(bot.name).toBe("Revisor de código");
32+
expect(bot.instructions).toBe(sheet.instructions);
33+
expect(bot.effort).toBe("high");
34+
expect(bot.permissionMode).toBe("default");
35+
expect(seen).toContain("bot.changed");
36+
37+
await expect(
38+
fake.call("catalog.add", {
39+
crewId: crew.id,
40+
templateId: "code-reviewer",
41+
name: "Revisor de código",
42+
role: "x",
43+
}),
44+
).rejects.toThrow();
45+
});
46+
});

‎app/src/lib/fakeCatalog.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// The fake daemon's bot catalog (spec 26): the same twelve ids and
2+
// categories as the daemon's sheets, with short stand-in texts. Adding a
3+
// role goes through `bots.create`, so handles and notifications match.
4+
5+
import type { FakeBotloft, Handlers } from "./fake";
6+
import { botHandlers } from "./fakeBots";
7+
import { notFound } from "./fakeRules";
8+
import type { BotEffort, BotTemplateCategory, BotTemplateFull } from "./protocol.gen";
9+
10+
type CatalogMethods = Extract<keyof Handlers, `catalog.${string}`>;
11+
12+
const ROLES: [id: string, category: BotTemplateCategory, name: string, effort?: BotEffort][] = [
13+
["developer", "code", "Developer"],
14+
["code-reviewer", "code", "Code Reviewer", "high"],
15+
["qa-tester", "code", "QA Tester"],
16+
["designer", "design", "Designer"],
17+
["writer", "content", "Writer"],
18+
["social-media", "content", "Social Media"],
19+
["translator", "content", "Translator"],
20+
["researcher", "research", "Researcher"],
21+
["data-analyst", "research", "Data Analyst"],
22+
["sales-prospector", "business", "Sales Prospector"],
23+
["customer-support", "business", "Customer Support"],
24+
["personal-assistant", "business", "Personal Assistant"],
25+
];
26+
27+
/** Every role, as `catalog.get` returns it. */
28+
export const FAKE_CATALOG: BotTemplateFull[] = ROLES.map(([id, category, name, effort]) => ({
29+
id,
30+
category,
31+
name,
32+
role: `${name} role`,
33+
summary: `What the ${name.toLowerCase()} does`,
34+
model: "default",
35+
effort: effort ?? "default",
36+
instructions: `You are a ${name.toLowerCase()} on this crew.\n\n### What you do\nThe ${name.toLowerCase()}'s work.`,
37+
}));
38+
39+
function find(id: string): BotTemplateFull {
40+
const found = FAKE_CATALOG.find((role) => role.id === id);
41+
if (!found) {
42+
throw notFound(`bot template ${id}`);
43+
}
44+
return found;
45+
}
46+
47+
export function catalogHandlers(fake: FakeBotloft): Pick<Handlers, CatalogMethods> {
48+
return {
49+
"catalog.list": ({ category }) =>
50+
FAKE_CATALOG.filter((role) => category === undefined || role.category === category).map(
51+
({ id, category, name, role, summary }) => ({ id, category, name, role, summary }),
52+
),
53+
"catalog.get": ({ id }) => find(id),
54+
"catalog.add": ({ crewId, templateId, name, role }) => {
55+
const template = find(templateId);
56+
const created = botHandlers(fake)["bots.create"]({
57+
crewId,
58+
name,
59+
role,
60+
instructions: template.instructions,
61+
model: template.model,
62+
});
63+
const bot = fake.bot(created.id);
64+
bot.effort = template.effort;
65+
return fake.changedBot(bot);
66+
},
67+
};
68+
}

‎app/src/lib/protocol.gen.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,36 @@ export type CrewAccessListParams = { botId: BotId, };
11331133

11341134
export type CrewAccessIdParams = { accessId: CrewAccessId, };
11351135

1136+
/**
1137+
* The group a role is listed under.
1138+
*/
1139+
export type BotTemplateCategory = "code" | "design" | "content" | "research" | "business";
1140+
1141+
/**
1142+
* A role as the list shows it, in English. The app writes the name, role
1143+
* and summary the owner reads in the owner's language (spec 26.2).
1144+
*/
1145+
export type BotTemplate = {
1146+
/**
1147+
* The sheet's file name: `code-reviewer`.
1148+
*/
1149+
id: string, category: BotTemplateCategory, name: string, role: string, summary: string, };
1150+
1151+
/**
1152+
* A role with what a bot made from it starts with.
1153+
*/
1154+
export type BotTemplateFull = { id: string, category: BotTemplateCategory, name: string, role: string, summary: string, model: BotModel, effort: BotEffort, instructions: string, };
1155+
1156+
export type CatalogListParams = { category?: BotTemplateCategory, };
1157+
1158+
export type CatalogGetParams = { id: string, };
1159+
1160+
export type CatalogAddParams = { crewId: CrewId, templateId: string,
1161+
/**
1162+
* In the owner's language, as the app writes it.
1163+
*/
1164+
name: string, role: string, };
1165+
11361166
export type BackupCrew = { name: string,
11371167
/**
11381168
* The crew's bots, archived ones aside.
@@ -1879,6 +1909,9 @@ export interface RpcMethods {
18791909
"bot.mcp.set": { params: BotMcpSetParams; result: BotMcp };
18801910
"crewAccess.list": { params: CrewAccessListParams; result: Array<CrewAccess> };
18811911
"crewAccess.revoke": { params: CrewAccessIdParams; result: Array<CrewAccess> };
1912+
"catalog.list": { params: CatalogListParams; result: Array<BotTemplate> };
1913+
"catalog.get": { params: CatalogGetParams; result: BotTemplateFull };
1914+
"catalog.add": { params: CatalogAddParams; result: Bot };
18821915
"backup.export": { params: BackupExportParams; result: BackupExported };
18831916
"backup.stage": { params: BackupStageParams; result: BackupManifest };
18841917
"backup.confirm": { params: undefined; result: null };
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//! The bot catalog (spec 26): ready-made roles the owner adds to a crew and
2+
//! the chief suggests from.
3+
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::ids::CrewId;
7+
8+
text_enum!(
9+
/// The group a role is listed under.
10+
BotTemplateCategory, "template category" {
11+
Code => "code",
12+
Design => "design",
13+
Content => "content",
14+
Research => "research",
15+
Business => "business",
16+
}
17+
);
18+
19+
/// A role as the list shows it, in English. The app writes the name, role
20+
/// and summary the owner reads in the owner's language (spec 26.2).
21+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22+
#[serde(rename_all = "camelCase")]
23+
#[cfg_attr(test, derive(ts_rs::TS))]
24+
pub struct BotTemplate {
25+
/// The sheet's file name: `code-reviewer`.
26+
pub id: String,
27+
pub category: BotTemplateCategory,
28+
pub name: String,
29+
pub role: String,
30+
pub summary: String,
31+
}
32+
33+
/// A role with what a bot made from it starts with.
34+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35+
#[serde(rename_all = "camelCase")]
36+
#[cfg_attr(test, derive(ts_rs::TS))]
37+
pub struct BotTemplateFull {
38+
pub id: String,
39+
pub category: BotTemplateCategory,
40+
pub name: String,
41+
pub role: String,
42+
pub summary: String,
43+
pub model: super::BotModel,
44+
pub effort: super::BotEffort,
45+
pub instructions: String,
46+
}
47+
48+
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
49+
#[serde(rename_all = "camelCase")]
50+
#[cfg_attr(test, derive(ts_rs::TS))]
51+
pub struct CatalogListParams {
52+
#[serde(default)]
53+
#[cfg_attr(test, ts(optional))]
54+
pub category: Option<BotTemplateCategory>,
55+
}
56+
57+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58+
#[serde(rename_all = "camelCase")]
59+
#[cfg_attr(test, derive(ts_rs::TS))]
60+
pub struct CatalogGetParams {
61+
pub id: String,
62+
}
63+
64+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65+
#[serde(rename_all = "camelCase")]
66+
#[cfg_attr(test, derive(ts_rs::TS))]
67+
pub struct CatalogAddParams {
68+
pub crew_id: CrewId,
69+
pub template_id: String,
70+
/// In the owner's language, as the app writes it.
71+
pub name: String,
72+
pub role: String,
73+
}

‎crates/botloft-core/src/protocol/export.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use crate::ids::{
1818
mod backup;
1919
#[path = "export_browser.rs"]
2020
mod browser;
21+
#[path = "export_catalog.rs"]
22+
mod catalog;
2123
#[path = "export_crew_access.rs"]
2224
mod crew_access;
2325
#[path = "export_desktop.rs"]
@@ -190,6 +192,7 @@ fn export_bindings() {
190192
desktop::decls(&mut out);
191193
mcp::decls(&mut out);
192194
crew_access::decls(&mut out);
195+
catalog::decls(&mut out);
193196
backup::decls(&mut out);
194197
out.decl::<ApprovalItem>();
195198
usage::decls(&mut out);
@@ -362,6 +365,7 @@ fn export_bindings() {
362365
desktop::methods(&mut out);
363366
mcp::methods(&mut out);
364367
crew_access::methods(&mut out);
368+
catalog::methods(&mut out);
365369
backup::methods(&mut out);
366370
routines::methods(&mut out);
367371
questions::methods(&mut out);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//! The bot catalog part of the TypeScript bindings (spec 26).
2+
3+
use super::super::*;
4+
use super::Out;
5+
6+
pub(super) fn decls(out: &mut Out) {
7+
out.decl::<BotTemplateCategory>();
8+
out.decl::<BotTemplate>();
9+
out.decl::<BotTemplateFull>();
10+
out.decl::<CatalogListParams>();
11+
out.decl::<CatalogGetParams>();
12+
out.decl::<CatalogAddParams>();
13+
}
14+
15+
pub(super) fn methods(out: &mut Out) {
16+
out.method(
17+
method::CATALOG_LIST,
18+
&out.name::<CatalogListParams>(),
19+
&out.name::<Vec<BotTemplate>>(),
20+
);
21+
out.method(
22+
method::CATALOG_GET,
23+
&out.name::<CatalogGetParams>(),
24+
&out.name::<BotTemplateFull>(),
25+
);
26+
out.method(
27+
method::CATALOG_ADD,
28+
&out.name::<CatalogAddParams>(),
29+
&out.name::<Bot>(),
30+
);
31+
}

‎crates/botloft-core/src/protocol/mod.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ macro_rules! text_enum {
5252
mod backup;
5353
mod browser;
5454
mod browser_input;
55+
mod catalog;
5556
mod chat;
5657
mod crew_access;
5758
mod desktop;
@@ -76,6 +77,7 @@ mod usage;
7677
pub use backup::*;
7778
pub use browser::*;
7879
pub use browser_input::*;
80+
pub use catalog::*;
7981
pub use chat::*;
8082
pub use crew_access::*;
8183
pub use desktop::*;
@@ -116,6 +118,9 @@ pub mod method {
116118
pub const ARCHIVE_LIST: &str = "archive.list";
117119
pub const BOTS_LIST: &str = "bots.list";
118120
pub const BOTS_CREATE: &str = "bots.create";
121+
pub const CATALOG_LIST: &str = "catalog.list";
122+
pub const CATALOG_GET: &str = "catalog.get";
123+
pub const CATALOG_ADD: &str = "catalog.add";
119124
pub const BOTS_UPDATE: &str = "bots.update";
120125
pub const BOTS_SET_PAUSED: &str = "bots.setPaused";
121126
pub const BOTS_SET_PERMISSION_MODE: &str = "bots.setPermissionMode";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
category = "code"
2+
name = "Code Reviewer"
3+
role = "Reviews what other bots changed and points out bugs, risks and needless complexity"
4+
summary = "Reads changes with a critical eye and says what is wrong, risky or too complicated"
5+
model = "default"
6+
effort = "high"
7+
instructions = """
8+
You are a code reviewer on this crew. Answer in the language the owner writes in.
9+
10+
### What you do
11+
You read changes that someone else made and say what is wrong with them: bugs, missed cases, security problems, missing tests and code that is more complicated than it needs to be. You do not edit the code yourself.
12+
13+
### How you work
14+
1. Find out what the change is meant to do (the task, the message that asked for it) before judging how it does it.
15+
2. Read the whole change, then the code around it that it depends on. A line that looks fine alone can break something elsewhere.
16+
3. Run the tests and the build if you can, and try the case that worries you most.
17+
4. For each problem, be sure it is real before you report it. Say how you know: the line, the input that breaks it, or the test that fails.
18+
5. Separate what must be fixed from what is only a suggestion. Do not pad the review with taste.
19+
20+
### What you hand back
21+
A list ordered from most to least serious. For each item: where it is (file and line), what goes wrong and when, and how you would fix it. Then a line on what looked good and a line on what you could not check. If you found nothing serious, say so plainly.
22+
23+
### Ask the owner first
24+
If you cannot tell which change to review, or the intended behavior is unclear, ask with `ask_owner` before reviewing. Never change the code to "help"; report it.
25+
26+
### With the crew
27+
Send your review to the bot that made the change with `send_message`. When another bot sends you a task, finish it with `complete_task` and put the review in it. If the crew needs a specialist it does not have, ask the chief.
28+
29+
### Privacy
30+
What you read while reviewing (code, data, secrets that slipped in) is private to the owner. If you find a secret in the code, say where it is, but do not repeat its value in messages.
31+
"""
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
category = "business"
2+
name = "Customer Support"
3+
role = "Answers customers' questions from the material the owner gave it and passes on what it cannot answer"
4+
summary = "Answers questions kindly using your own material, and hands you what it cannot answer"
5+
model = "default"
6+
effort = "default"
7+
instructions = """
8+
You are a customer support assistant on this crew. Answer in the language the owner writes in, and draft customer replies in the language the customer used.
9+
10+
### What you do
11+
You help answer customers' questions and solve their problems, using the material the owner gave you: product information, policies, past answers and any connected tool for the support system. When you do not know, you say so and pass it on.
12+
13+
### How you work
14+
1. Read the material the owner gave you before answering. Answer only from it, from the customer's message or from what a tool shows you. Never invent a policy, a price, a date or a promise.
15+
2. Read the whole message, find what the customer really needs and answer that first, then the rest.
16+
3. Write in a warm, direct tone. Short sentences, no jargon, no blame. If the customer is upset, acknowledge it once and move to the solution.
17+
4. If the answer needs a decision you cannot make (a refund, an exception, a legal question), do not guess: prepare the case and pass it on.
18+
5. Notice what customers ask again and again, and suggest to the owner what to add to the material.
19+
20+
### What you hand back
21+
A draft reply ready to send, and a one-line note for the owner: what the customer wanted, what you based the answer on and whether you are sure. For cases you cannot solve, a short summary of the case and what is missing.
22+
23+
### Ask the owner first
24+
Ask with `ask_owner` before you promise anything, offer a refund or discount, share personal data, or send a reply, unless the owner has told you that you may send routine replies. When a connected tool could send, show the draft and wait for the owner's yes the first times.
25+
26+
### With the crew
27+
Send technical problems to a developer and wording questions to a writer with `send_message`. When another bot sends you a task, finish it with `complete_task` and put the result in it. If the crew needs a specialist it does not have, ask the chief.
28+
29+
### Privacy
30+
Customers' messages hold personal data. Use it only to help that customer, never in replies to someone else, and do not copy more of it into messages to other bots than a task needs.
31+
"""

0 commit comments

Comments
 (0)