Skip to content

Commit 8c9ad4c

Browse files
authored
Public-web research by role: Exa and Firecrawl beside Parallel (#790)
* Describe public-web research by role, and add Exa and Firecrawl Research guidance and the Research Desk skill were written for Parallel's two tools by name. A catalogue entry now says which of its tools search and which fetch (`research`), the guidance is built from whichever providers a Bot holds, and a skill can declare `@research/search` and `@research/fetch` to match any provider it was granted. Exa and Firecrawl join Parallel, each free and with an API key. Exa and Firecrawl list the tools that only read (`readTools`), and anything else they advertise is a write: Firecrawl publishes tool families such as firecrawl_monitor_* that no write list could name. A deployment key can travel in a vendor's own header, as Exa reads x-api-key. * Offer research roles when writing a skill The Skills page gains a "Public-web research" group for the two roles, instead of listing them as not connected, and list_skill_tools tells the authoring assistant about them. The admin page says a deployment key is sent with every call, since it is no longer always a bearer token. * Document public-web research across providers docs/parallel-research.md becomes docs/web-research.md, covering Parallel, Exa and Firecrawl, the research roles, and how to add a provider. * Search for facts that change when a Bot holds a search tool Every Bot was told "this is not an instruction to go looking" and not to hunt the open web, which was written for Bots whose only way to look was their browser. So General Assistant, holding Exa, charted the top five economies from memory, and asked whether it could search, said yes. The provenance guidance now leaves finding sources to a search tool, the research guidance tells a Bot to search before answering about current figures, rankings, prices or recent events, a search or two at most, and the browser guidance says opening a page is not a web search. * Name research calls for what they did, not for the provider The transcript drew mcp__exa__web_search_exa as "Web search exa", telling the person which vendor the deployment buys search from. Research calls now read "Search the web" and "Read web pages" whichever provider made them, and the Bot is told not to name the provider unless asked. The provider tool table moves to shared/research.ts so the app can recognise these tools; a test keeps its keys in step with the catalogue. The Bot's access page still groups them under the provider. * Fold a reply's tool calls into one line (#791) * Fold a reply's tool calls into one line Adjacent connector calls and browser steps used to draw a line each above the answer. They now fold into one row that says what was done ("Searched the web and read 2 pages"), with an icon per kind of step, the running step's name while it works, and a count of blocked or failed steps. Opened, each step shows its query, page or connector, and opens to its inputs and output. The browser grouping this replaces folded only browser steps; groupToolSteps folds both. A single connector call keeps its own line, and a Team Bot consent request, a tool the app draws itself, and any pending approval stay outside the fold. * Draw a tool group's icons at a readable size The stack's icons were 11px in 20px circles and the step icons 13px, too small to tell a search from a page at a glance. Stack icons are now 15px in 24px circles and step icons 15px.
1 parent d7cc57e commit 8c9ad4c

35 files changed

Lines changed: 1412 additions & 285 deletions

‎CHANGELOG.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,43 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
3232
`code_required` (below). A server and a computer must be upgraded together: an older computer has
3333
no `/sign-in/code`.
3434

35+
### A reply's tool calls fold into one line
36+
37+
A Bot that searched, read two pages and checked Drive before answering drew four lines above its
38+
answer. Adjacent connector calls and browser steps now fold into one line that says what was done,
39+
such as "Searched the web and read 2 pages", with an icon for each kind of step. It names the step in
40+
progress while the Bot works, and counts issues when a step was blocked or failed. Opened, it lists
41+
each step with its query, page or connector, and each step opens to what it was given and what came
42+
back. A single connector call keeps its own line, and approvals, connection requests and anything the
43+
app draws itself stay outside the fold.
44+
45+
### Public-web research works with Exa and Firecrawl, not only Parallel
46+
47+
Four new catalogue entries give a Bot public-web search and page reading without its browser:
48+
**Exa Search** (free, rate-limited), **Exa Search (API key)**, **Firecrawl** (free) and **Firecrawl
49+
(API key)**. They sit beside the two Parallel entries, and nothing is granted to any Bot until an administrator does it.
50+
See [docs/web-research.md](docs/web-research.md).
51+
52+
- **The research guidance follows whichever provider a Bot holds.** It used to switch on only for
53+
Parallel's two tools. Now a Bot holding any provider's search and fetch tools is told how to use
54+
them, and a Bot holding more than one uses the one the person asks for.
55+
- **A Bot holding a research provider searches without being asked** when an answer depends on facts
56+
that change, such as current figures, rankings or recent events, and stops after a search or two.
57+
Before, every Bot was told not to go looking, so one holding a search tool still answered from
58+
memory. A Bot with only its browser now says it can open pages but cannot search.
59+
- **A conversation doesn't say which provider searched.** Research calls show as "Search the web"
60+
and "Read web pages" rather than "Web search exa", and the Bot doesn't name the provider unless
61+
asked. The Bot's access page still shows which provider it holds.
62+
- **The shipped `research-public-web` skill declares roles, not Parallel.** Its tools are now
63+
`@research/search` and `@research/fetch`, which match the search and fetch tools of every research
64+
provider the Bot was granted. A skill written by a person can declare the same roles, and the
65+
Skills page offers them under "Public-web research". A role grants nothing.
66+
- **Exa and Firecrawl list their reads.** Every other tool they advertise counts as a write, so a
67+
Firecrawl monitor deletion or an Exa agent run goes through the same policy as any write.
68+
- **An API key can go in the vendor's own header.** Exa reads `x-api-key`; the deployment key for
69+
that entry is sent there. The admin page now says the key is "Sent with every call to this vendor"
70+
rather than calling it a bearer token.
71+
3572
### Bot turns hold their conversation more carefully
3673

3774
Routines, responsibilities, background research, wake-ups, group turns and Bot-to-Bot handoffs keep a

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you
226226
- **Secrets never enter the transcript**: the trail records that a secret was requested and how long it was, not what it said.
227227
- **Bring your own agent**: any AG-UI endpoint is a Bot, on a framework or hand-written. Endpoints are validated with the same target checks used for browser navigation, and an auth header is stored write-only.
228228
- **Components instead of prose**: compiled React components live in `app/src/components/gallery/`, sandboxed ones are authored in `/admin/playground` and published with no deployment. Every call asks the server whether the component exists, is published, and is not withheld from that Bot. Data functions are granted per component.
229-
- **Governed MCP**: Google Drive, Notion and [Parallel Search](docs/parallel-research.md) ship in the catalogue, and Composio brokers a few hundred more apps behind one account, each reached as the person asking. The catalogue carries only vendors this deployment stands behind, so adding one is a review of that vendor. Custom servers must pass URL checks; unknown tools and custom-server tools are treated as writes, and a catalogue tool the server advertises but does not name as a write classifies as a read. A Bot is told which connectors exist here and which it holds, so it says it has not been granted one rather than browsing to the vendor's website.
229+
- **Governed MCP**: Google Drive, Notion and the [public-web research providers](docs/web-research.md) Parallel, Exa and Firecrawl ship in the catalogue, and Composio brokers a few hundred more apps behind one account, each reached as the person asking. The catalogue carries only vendors this deployment stands behind, so adding one is a review of that vendor. Custom servers must pass URL checks; unknown tools and custom-server tools are treated as writes, and a catalogue tool the server advertises but does not name as a write classifies as a read. A Bot is told which connectors exist here and which it holds, so it says it has not been granted one rather than browsing to the vendor's website.
230230
- **Skills are instructions, not capabilities**: personal skills attach only to Bots their author owns, deployment skills are admin-owned, and both are invoked with `/` in the composer. A Bot granted the shipped `skill-creator` skill can write one with you in the conversation, and saves it only when you press the button on the card.
231231
- **Sign in with what your company already has**: Google, Microsoft or Okta from the environment, or a company's own SAML or OpenID Connect provider registered while the deployment runs and routed by email domain. Any one turns sign-in on; several may be configured at once.
232232
- **Decide who gets in**: `/admin/people` lists everybody who has signed in, promotes and demotes them, and removes access, which ends the session they are using and stops the next sign-in. Every change is on the audit trail.
@@ -445,7 +445,7 @@ Use `bash scripts/start.sh` for the whole stack and `bash scripts/stop.sh` to ta
445445
- [docs/deployment.md](docs/deployment.md)
446446
- [charts/openbot/README.md](charts/openbot/README.md): the Helm chart
447447
- [docs/releasing.md](docs/releasing.md)
448-
- [docs/parallel-research.md](docs/parallel-research.md): public-web search and extraction through Parallel
448+
- [docs/web-research.md](docs/web-research.md): public-web search and extraction through Parallel, Exa or Firecrawl
449449

450450
## Contributing
451451

Lines changed: 3 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import { PendingApprovals } from "@/components/approvals/inline-approval";
21
import type { VisibleChatItem } from "./chat-messages";
3-
import { ToolLine } from "./tool-line";
42

53
type BrowserStep = Extract<VisibleChatItem, { kind: "tool" }>;
6-
export type BrowserGroup = {
7-
kind: "browser";
8-
id: string;
9-
steps: BrowserStep[];
10-
};
11-
export type TranscriptItem = VisibleChatItem | BrowserGroup;
124

135
const labels: Record<string, string> = {
146
computer_navigate: "Opened page",
@@ -20,24 +12,9 @@ const labels: Record<string, string> = {
2012
computer_scroll: "Scrolled page",
2113
};
2214

23-
/** Group adjacent browsing steps without moving messages or hiding requests for human help. */
24-
export function groupBrowserSteps(
25-
items: readonly VisibleChatItem[],
26-
): TranscriptItem[] {
27-
const grouped: TranscriptItem[] = [];
28-
for (const item of items) {
29-
if (
30-
item.kind !== "tool" ||
31-
!Object.hasOwn(labels, item.toolCall.function.name)
32-
) {
33-
grouped.push(item);
34-
continue;
35-
}
36-
const last = grouped.at(-1);
37-
if (last?.kind === "browser") last.steps.push(item);
38-
else grouped.push({ kind: "browser", id: item.id, steps: [item] });
39-
}
40-
return grouped;
15+
/** Whether a tool call is one of the browser steps a tool group draws with its page link. */
16+
export function isBrowserStep(item: BrowserStep): boolean {
17+
return Object.hasOwn(labels, item.toolCall.function.name);
4118
}
4219

4320
function objectOf(value: string | undefined): Record<string, unknown> {
@@ -83,72 +60,3 @@ export function browserStepDetails(step: BrowserStep) {
8360
pending: step.result === undefined,
8461
};
8562
}
86-
87-
export function BrowserActivity({
88-
group,
89-
active,
90-
}: {
91-
group: BrowserGroup;
92-
active: boolean;
93-
}) {
94-
const steps = group.steps.map((step) => ({
95-
id: step.id,
96-
...browserStepDetails(step),
97-
}));
98-
const pages = steps.filter((step) => step.visited).length;
99-
const issues = steps.filter((step) => step.failed).length;
100-
const running = active && steps.some((step) => step.pending);
101-
// The folded line draws its own steps, so a waiting approval gets its card beside it, not inside.
102-
return (
103-
<>
104-
<ToolLine
105-
label={
106-
running
107-
? "Browsing…"
108-
: pages
109-
? `Browsed ${pages} ${pages === 1 ? "page" : "pages"}`
110-
: "Browser activity"
111-
}
112-
detail={
113-
issues ? `${issues} ${issues === 1 ? "issue" : "issues"}` : undefined
114-
}
115-
running={running}
116-
>
117-
<ol className="space-y-2 py-1">
118-
{steps.map((step) => (
119-
<li key={step.id} className="min-w-0">
120-
<div className="flex min-w-0 items-baseline gap-2">
121-
<span className="shrink-0 text-muted-foreground">
122-
{step.label}
123-
</span>
124-
{step.href ? (
125-
<a
126-
href={step.href}
127-
target="_blank"
128-
rel="noopener noreferrer"
129-
className="truncate underline decoration-muted-foreground/40 underline-offset-2 hover:decoration-current"
130-
>
131-
{step.title ?? step.href}
132-
</a>
133-
) : null}
134-
{step.pending && (
135-
<span className="text-muted-foreground">
136-
{active ? "In progress" : "Not completed"}
137-
</span>
138-
)}
139-
</div>
140-
{step.failed && (
141-
<p className="mt-0.5 text-destructive">
142-
{step.reason ?? "This step could not be completed."}
143-
</p>
144-
)}
145-
</li>
146-
))}
147-
</ol>
148-
</ToolLine>
149-
<PendingApprovals
150-
toolCallIds={group.steps.map((step) => step.toolCall.id)}
151-
/>
152-
</>
153-
);
154-
}

‎app/src/components/channels/chat-transcript.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ import { readTeamBotConsent } from "@/lib/team-bots";
6262
import { cn } from "@/lib/utils";
6363
import { VOICE_CHAT_ACTIVITY, type VoiceChatEntry } from "@/lib/voice/archive";
6464
import {
65-
BrowserActivity,
66-
groupBrowserSteps,
65+
groupToolSteps,
66+
ToolActivity,
6767
type TranscriptItem,
68-
} from "./browser-activity";
68+
} from "./tool-activity";
6969
import {
7070
attachmentModality,
7171
type SentAttachment,
@@ -1525,7 +1525,7 @@ export function ChatTranscript({
15251525
* cost was markdown parsing and chart SVGs, and those are skipped by the memoised children below,
15261526
* which is where the 25x came from. This runs per render and is not worth guarding.
15271527
*/
1528-
const items = groupBrowserSteps(toVisibleChatItems(messages));
1528+
const items = groupToolSteps(toVisibleChatItems(messages));
15291529

15301530
/*
15311531
* ONLY WHILE THERE IS NOTHING ELSE TO LOOK AT. Once a reply starts streaming, or a tool line
@@ -1668,12 +1668,12 @@ export function ChatTranscript({
16681668
</div>
16691669
{items.length === 0 && restoring ? <RestoringTranscript /> : null}
16701670
{items.map((item, index) =>
1671-
item.kind === "browser" ? (
1671+
item.kind === "tools" ? (
16721672
<MessageScrollerItem key={item.id} messageId={item.id}>
16731673
<Arriving
16741674
delay={delays.delayFor(item.id, index, items.length)}
16751675
>
1676-
<BrowserActivity
1676+
<ToolActivity
16771677
group={item}
16781678
active={busy && index === items.length - 1}
16791679
/>

0 commit comments

Comments
 (0)