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
2 changes: 2 additions & 0 deletions app/src/lib/fake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FakeAllow } from "./fakeAllow";
import { backupHandlers } from "./fakeBackup";
import { botHandlers } from "./fakeBots";
import { FakeBrowser } from "./fakeBrowser";
import { catalogHandlers } from "./fakeCatalog";
import { FakeChat } from "./fakeChat";
import { FakeConversation } from "./fakeConversation";
import { FakeCrewAccess } from "./fakeCrewAccess";
Expand Down Expand Up @@ -272,6 +273,7 @@ export class FakeBotloft implements BotloftApi {
}),
...crewHandlers(this),
...botHandlers(this),
...catalogHandlers(this),
...this.chat.handlers(),
...this.allow.handlers(),
...this.crewAccess.handlers(),
Expand Down
46 changes: 46 additions & 0 deletions app/src/lib/fakeCatalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { FakeBotloft } from "./fake";

describe("the fake's bot catalog", () => {
it("lists the roles, by category too, and reads one with its instructions", async () => {
const fake = new FakeBotloft();
expect(await fake.call("catalog.list", {})).toHaveLength(12);
const code = await fake.call("catalog.list", { category: "code" });
expect(code.map((role) => role.id)).toEqual(["developer", "code-reviewer", "qa-tester"]);
expect(code[0]).not.toHaveProperty("instructions");

const reviewer = await fake.call("catalog.get", { id: "code-reviewer" });
expect(reviewer.effort).toBe("high");
expect(reviewer.instructions).toContain("### What you do");
await expect(fake.call("catalog.get", { id: "wizard" })).rejects.toThrow();
});

it("adds a role as a plain bot, with the name and role the app wrote", async () => {
const fake = new FakeBotloft();
const crew = fake.addCrew("Studio");
const seen: string[] = [];
fake.subscribe((event) => seen.push(event.name));

const bot = await fake.call("catalog.add", {
crewId: crew.id,
templateId: "code-reviewer",
name: "Revisor de código",
role: "Revisa o que os outros bots mudaram",
});
const sheet = await fake.call("catalog.get", { id: "code-reviewer" });
expect(bot.name).toBe("Revisor de código");
expect(bot.instructions).toBe(sheet.instructions);
expect(bot.effort).toBe("high");
expect(bot.permissionMode).toBe("default");
expect(seen).toContain("bot.changed");

await expect(
fake.call("catalog.add", {
crewId: crew.id,
templateId: "code-reviewer",
name: "Revisor de código",
role: "x",
}),
).rejects.toThrow();
});
});
68 changes: 68 additions & 0 deletions app/src/lib/fakeCatalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// The fake daemon's bot catalog (spec 26): the same twelve ids and
// categories as the daemon's sheets, with short stand-in texts. Adding a
// role goes through `bots.create`, so handles and notifications match.

import type { FakeBotloft, Handlers } from "./fake";
import { botHandlers } from "./fakeBots";
import { notFound } from "./fakeRules";
import type { BotEffort, BotTemplateCategory, BotTemplateFull } from "./protocol.gen";

type CatalogMethods = Extract<keyof Handlers, `catalog.${string}`>;

const ROLES: [id: string, category: BotTemplateCategory, name: string, effort?: BotEffort][] = [
["developer", "code", "Developer"],
["code-reviewer", "code", "Code Reviewer", "high"],
["qa-tester", "code", "QA Tester"],
["designer", "design", "Designer"],
["writer", "content", "Writer"],
["social-media", "content", "Social Media"],
["translator", "content", "Translator"],
["researcher", "research", "Researcher"],
["data-analyst", "research", "Data Analyst"],
["sales-prospector", "business", "Sales Prospector"],
["customer-support", "business", "Customer Support"],
["personal-assistant", "business", "Personal Assistant"],
];

/** Every role, as `catalog.get` returns it. */
export const FAKE_CATALOG: BotTemplateFull[] = ROLES.map(([id, category, name, effort]) => ({
id,
category,
name,
role: `${name} role`,
summary: `What the ${name.toLowerCase()} does`,
model: "default",
effort: effort ?? "default",
instructions: `You are a ${name.toLowerCase()} on this crew.\n\n### What you do\nThe ${name.toLowerCase()}'s work.`,
}));

function find(id: string): BotTemplateFull {
const found = FAKE_CATALOG.find((role) => role.id === id);
if (!found) {
throw notFound(`bot template ${id}`);
}
return found;
}

export function catalogHandlers(fake: FakeBotloft): Pick<Handlers, CatalogMethods> {
return {
"catalog.list": ({ category }) =>
FAKE_CATALOG.filter((role) => category === undefined || role.category === category).map(
({ id, category, name, role, summary }) => ({ id, category, name, role, summary }),
),
"catalog.get": ({ id }) => find(id),
"catalog.add": ({ crewId, templateId, name, role }) => {
const template = find(templateId);
const created = botHandlers(fake)["bots.create"]({
crewId,
name,
role,
instructions: template.instructions,
model: template.model,
});
const bot = fake.bot(created.id);
bot.effort = template.effort;
return fake.changedBot(bot);
},
};
}
33 changes: 33 additions & 0 deletions app/src/lib/protocol.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,36 @@ export type CrewAccessListParams = { botId: BotId, };

export type CrewAccessIdParams = { accessId: CrewAccessId, };

/**
* The group a role is listed under.
*/
export type BotTemplateCategory = "code" | "design" | "content" | "research" | "business";

/**
* A role as the list shows it, in English. The app writes the name, role
* and summary the owner reads in the owner's language (spec 26.2).
*/
export type BotTemplate = {
/**
* The sheet's file name: `code-reviewer`.
*/
id: string, category: BotTemplateCategory, name: string, role: string, summary: string, };

/**
* A role with what a bot made from it starts with.
*/
export type BotTemplateFull = { id: string, category: BotTemplateCategory, name: string, role: string, summary: string, model: BotModel, effort: BotEffort, instructions: string, };

export type CatalogListParams = { category?: BotTemplateCategory, };

export type CatalogGetParams = { id: string, };

export type CatalogAddParams = { crewId: CrewId, templateId: string,
/**
* In the owner's language, as the app writes it.
*/
name: string, role: string, };

export type BackupCrew = { name: string,
/**
* The crew's bots, archived ones aside.
Expand Down Expand Up @@ -1879,6 +1909,9 @@ export interface RpcMethods {
"bot.mcp.set": { params: BotMcpSetParams; result: BotMcp };
"crewAccess.list": { params: CrewAccessListParams; result: Array<CrewAccess> };
"crewAccess.revoke": { params: CrewAccessIdParams; result: Array<CrewAccess> };
"catalog.list": { params: CatalogListParams; result: Array<BotTemplate> };
"catalog.get": { params: CatalogGetParams; result: BotTemplateFull };
"catalog.add": { params: CatalogAddParams; result: Bot };
"backup.export": { params: BackupExportParams; result: BackupExported };
"backup.stage": { params: BackupStageParams; result: BackupManifest };
"backup.confirm": { params: undefined; result: null };
Expand Down
73 changes: 73 additions & 0 deletions crates/botloft-core/src/protocol/catalog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! The bot catalog (spec 26): ready-made roles the owner adds to a crew and
//! the chief suggests from.

use serde::{Deserialize, Serialize};

use crate::ids::CrewId;

text_enum!(
/// The group a role is listed under.
BotTemplateCategory, "template category" {
Code => "code",
Design => "design",
Content => "content",
Research => "research",
Business => "business",
}
);

/// A role as the list shows it, in English. The app writes the name, role
/// and summary the owner reads in the owner's language (spec 26.2).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(ts_rs::TS))]
pub struct BotTemplate {
/// The sheet's file name: `code-reviewer`.
pub id: String,
pub category: BotTemplateCategory,
pub name: String,
pub role: String,
pub summary: String,
}

/// A role with what a bot made from it starts with.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(ts_rs::TS))]
pub struct BotTemplateFull {
pub id: String,
pub category: BotTemplateCategory,
pub name: String,
pub role: String,
pub summary: String,
pub model: super::BotModel,
pub effort: super::BotEffort,
pub instructions: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(ts_rs::TS))]
pub struct CatalogListParams {
#[serde(default)]
#[cfg_attr(test, ts(optional))]
pub category: Option<BotTemplateCategory>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(ts_rs::TS))]
pub struct CatalogGetParams {
pub id: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(ts_rs::TS))]
pub struct CatalogAddParams {
pub crew_id: CrewId,
pub template_id: String,
/// In the owner's language, as the app writes it.
pub name: String,
pub role: String,
}
4 changes: 4 additions & 0 deletions crates/botloft-core/src/protocol/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use crate::ids::{
mod backup;
#[path = "export_browser.rs"]
mod browser;
#[path = "export_catalog.rs"]
mod catalog;
#[path = "export_crew_access.rs"]
mod crew_access;
#[path = "export_desktop.rs"]
Expand Down Expand Up @@ -190,6 +192,7 @@ fn export_bindings() {
desktop::decls(&mut out);
mcp::decls(&mut out);
crew_access::decls(&mut out);
catalog::decls(&mut out);
backup::decls(&mut out);
out.decl::<ApprovalItem>();
usage::decls(&mut out);
Expand Down Expand Up @@ -362,6 +365,7 @@ fn export_bindings() {
desktop::methods(&mut out);
mcp::methods(&mut out);
crew_access::methods(&mut out);
catalog::methods(&mut out);
backup::methods(&mut out);
routines::methods(&mut out);
questions::methods(&mut out);
Expand Down
31 changes: 31 additions & 0 deletions crates/botloft-core/src/protocol/export_catalog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! The bot catalog part of the TypeScript bindings (spec 26).

use super::super::*;
use super::Out;

pub(super) fn decls(out: &mut Out) {
out.decl::<BotTemplateCategory>();
out.decl::<BotTemplate>();
out.decl::<BotTemplateFull>();
out.decl::<CatalogListParams>();
out.decl::<CatalogGetParams>();
out.decl::<CatalogAddParams>();
}

pub(super) fn methods(out: &mut Out) {
out.method(
method::CATALOG_LIST,
&out.name::<CatalogListParams>(),
&out.name::<Vec<BotTemplate>>(),
);
out.method(
method::CATALOG_GET,
&out.name::<CatalogGetParams>(),
&out.name::<BotTemplateFull>(),
);
out.method(
method::CATALOG_ADD,
&out.name::<CatalogAddParams>(),
&out.name::<Bot>(),
);
}
5 changes: 5 additions & 0 deletions crates/botloft-core/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ macro_rules! text_enum {
mod backup;
mod browser;
mod browser_input;
mod catalog;
mod chat;
mod crew_access;
mod desktop;
Expand All @@ -76,6 +77,7 @@ mod usage;
pub use backup::*;
pub use browser::*;
pub use browser_input::*;
pub use catalog::*;
pub use chat::*;
pub use crew_access::*;
pub use desktop::*;
Expand Down Expand Up @@ -116,6 +118,9 @@ pub mod method {
pub const ARCHIVE_LIST: &str = "archive.list";
pub const BOTS_LIST: &str = "bots.list";
pub const BOTS_CREATE: &str = "bots.create";
pub const CATALOG_LIST: &str = "catalog.list";
pub const CATALOG_GET: &str = "catalog.get";
pub const CATALOG_ADD: &str = "catalog.add";
pub const BOTS_UPDATE: &str = "bots.update";
pub const BOTS_SET_PAUSED: &str = "bots.setPaused";
pub const BOTS_SET_PERMISSION_MODE: &str = "bots.setPermissionMode";
Expand Down
31 changes: 31 additions & 0 deletions crates/botloftd/catalog/code-reviewer.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
category = "code"
name = "Code Reviewer"
role = "Reviews what other bots changed and points out bugs, risks and needless complexity"
summary = "Reads changes with a critical eye and says what is wrong, risky or too complicated"
model = "default"
effort = "high"
instructions = """
You are a code reviewer on this crew. Answer in the language the owner writes in.

### What you do
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.

### How you work
1. Find out what the change is meant to do (the task, the message that asked for it) before judging how it does it.
2. Read the whole change, then the code around it that it depends on. A line that looks fine alone can break something elsewhere.
3. Run the tests and the build if you can, and try the case that worries you most.
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.
5. Separate what must be fixed from what is only a suggestion. Do not pad the review with taste.

### What you hand back
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.

### Ask the owner first
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.

### With the crew
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.

### Privacy
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 changes: 31 additions & 0 deletions crates/botloftd/catalog/customer-support.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
category = "business"
name = "Customer Support"
role = "Answers customers' questions from the material the owner gave it and passes on what it cannot answer"
summary = "Answers questions kindly using your own material, and hands you what it cannot answer"
model = "default"
effort = "default"
instructions = """
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.

### What you do
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.

### How you work
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.
2. Read the whole message, find what the customer really needs and answer that first, then the rest.
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.
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.
5. Notice what customers ask again and again, and suggest to the owner what to add to the material.

### What you hand back
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.

### Ask the owner first
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.

### With the crew
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.

### Privacy
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.
"""
Loading
Loading