Skip to content

Commit c99a1a5

Browse files
authored
Assign the openbot Learning container automatically (CopilotKit#774)
1 parent 17eb2a4 commit c99a1a5

28 files changed

Lines changed: 1592 additions & 56 deletions

‎.env.example‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,14 @@ COPILOTKIT_LICENSE_TOKEN=
144144
# Learning defaults on. Fresh managed setup: login, project select, then bun scripts/setup-learning.ts.
145145
# The helper creates/reuses openbot and writes the verified key and container; custom targets stay.
146146
# Fresh self-hosted setup: set its API URL, install Chrome/Edge, then run the same helper to sign in.
147-
# Existing key-only setup: create a container in that project before assigning its ID below.
147+
# With nothing saved in Admin and no container below, the server assigns the key's project's openbot
148+
# container by itself once it exists. The key cannot create it: run the helper (it keeps an existing
149+
# key) or create it in Intelligence. scripts/start.sh runs that check on every start, non-interactively.
148150
# Admin → Automatic Learning can enable/disable, override per Bot, and save settings in the DB.
149151
# Saved settings take precedence, including off. Without an assigned container, collection and
150152
# delivery stay inactive; OpenBot still starts and chats.
153+
# false or 0 stops the automatic openbot assignment. A container named below also stops it.
154+
# OPENBOT_LEARNING_AUTO_ASSIGN=false
151155
# Project credentials remain server-side; review and publish skills in the Intelligence UI.
152156
# CPK_INTELLIGENCE_LEARNING_CONTAINER_ID=support-learning
153157
# Optional opaque published revision ID; omit to follow latest.

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

11+
### Automatic Learning assigns its container without a trip to Admin
12+
13+
Learning was on by default but collected nothing until somebody assigned a container. Now, when
14+
nothing is saved under Admin → Automatic Learning and `CPK_INTELLIGENCE_LEARNING_CONTAINER_ID` is
15+
not set, the server assigns its Intelligence project's `openbot` container as the default as soon as
16+
that container exists, records it in the audit log, and the Admin page says so. A saved setting,
17+
including off, is never changed. The runtime key cannot create a container, so
18+
`bun scripts/setup-learning.ts` now creates `openbot` in an existing key's project instead of
19+
refusing, and `scripts/start.sh` runs that check on every start without a browser or prompt. Set
20+
`OPENBOT_LEARNING_AUTO_ASSIGN=false` (Helm: `config.learning.autoAssign: false`) to keep the old
21+
behaviour.
22+
1123
### Memory lives in CopilotKit Intelligence
1224

1325
A deployment entitled to Intelligence memory now keeps its people's memories there: Intelligence holds

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,16 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui),
9393

9494
The helper provisions the selected project's runtime key, creates or reuses its
9595
`openbot` Learning container, then writes both settings to `.env`. It preserves
96-
custom container assignments and refuses to replace an existing OpenBot key.
96+
custom container assignments and never replaces an existing OpenBot key; with one
97+
set, it only creates `openbot` in that key's project if it is missing.
9798
For fresh self-hosted setup, set `INTELLIGENCE_API_URL` to that deployment's
9899
HTTPS API origin (or `http://localhost` for a local one) and run the helper
99100
without the two `npx` commands. It opens an isolated Chrome or Edge window for
100101
your usual sign-in and project choice; install either browser first. It writes
101102
the API URL, the key and the container, not `INTELLIGENCE_GATEWAY_WS_URL`, so
102-
set that one to the same deployment yourself. Existing key-only setups can assign a container
103-
through [Admin → Automatic Learning](docs/automatic-learning.md).
103+
set that one to the same deployment yourself. Once `openbot` exists, the server assigns it
104+
as the default by itself unless [Admin → Automatic Learning](docs/automatic-learning.md)
105+
has a saved setting.
104106
Managed Intelligence needs no separate licence token.
105107

106108
3. Fill the remaining required values:

‎app/src/lib/learning/queries.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { queryOptions } from "@tanstack/react-query";
22
import { client } from "@/lib/client";
33
import type {
4+
LearningAutoAssignState,
45
LearningDeliveryStatus,
56
LearningOverview,
67
LearningSettings,
@@ -9,6 +10,7 @@ import type {
910
export const learningKeys = {
1011
all: ["learning"] as const,
1112
settings: ["learning", "settings"] as const,
13+
automatic: ["learning", "automatic"] as const,
1214
};
1315
export function learningSettingsQueryOptions() {
1416
return queryOptions({
@@ -19,6 +21,19 @@ export function learningSettingsQueryOptions() {
1921
}),
2022
});
2123
}
24+
/** Why no container was assigned automatically, or `null` when the server has nothing to say. */
25+
export function learningAutomaticQueryOptions() {
26+
return queryOptions({
27+
queryKey: learningKeys.automatic,
28+
retry: false,
29+
queryFn: async (): Promise<LearningAutoAssignState | null> =>
30+
(await client<LearningAutoAssignState | undefined>(
31+
"/api/admin/learning/settings",
32+
"automatic",
33+
{ fallback: "Learning settings could not be loaded." },
34+
)) ?? null,
35+
});
36+
}
2237
export function learningOverviewQueryOptions(agentId?: string) {
2338
return queryOptions({
2439
queryKey: ["learning", "overview", agentId],

‎app/src/routes/_authed/admin/learning.tsx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { Button } from "@/components/ui/button";
66
import { Input } from "@/components/ui/input";
77
import { agentListQueryOptions } from "@/lib/agents/queries";
88
import {
9+
learningAutomaticQueryOptions,
910
learningDeliveryQueryOptions,
1011
learningKeys,
1112
learningOverviewQueryOptions,
1213
learningSettingsQueryOptions,
1314
saveLearningSettings,
1415
} from "@/lib/learning/queries";
1516
import type {
17+
LearningAutoAssignState,
1618
LearningSettings,
1719
LearningTarget,
1820
} from "../../../../../shared/learning";
@@ -22,6 +24,26 @@ export const Route = createFileRoute("/_authed/admin/learning")({
2224
});
2325
const selectClass = "w-full rounded-md border bg-background px-3 py-2 text-sm";
2426

27+
/** What automatic assignment found, in words, or nothing when there is nothing to say. */
28+
function automaticMessage(
29+
automatic: LearningAutoAssignState | null | undefined,
30+
settings: LearningSettings,
31+
): string | undefined {
32+
if (!automatic) return undefined;
33+
if (
34+
automatic.state === "assigned" &&
35+
settings.defaultTarget?.containerId === automatic.containerId
36+
)
37+
return `OpenBot assigned this Intelligence project's "${automatic.containerId}" container automatically. Change it here at any time.`;
38+
if (automatic.state === "absent")
39+
return `This Intelligence project has no "${automatic.containerId}" Learning container yet, so nothing is collected. Create a container with that ID in Intelligence, or run bun scripts/setup-learning.ts, and OpenBot assigns it automatically.`;
40+
if (automatic.state === "not_entitled")
41+
return "This Intelligence project's plan does not include Learning, so no container was assigned.";
42+
if (automatic.state === "unavailable")
43+
return "OpenBot could not ask Intelligence for this project's Learning container. It tries again.";
44+
return undefined;
45+
}
46+
2547
function TargetFields({
2648
target,
2749
onChange,
@@ -76,6 +98,7 @@ function TargetFields({
7698
export function LearningPage() {
7799
const queryClient = useQueryClient();
78100
const settingsQuery = useQuery(learningSettingsQueryOptions());
101+
const automatic = useQuery(learningAutomaticQueryOptions());
79102
const agents = useQuery(agentListQueryOptions());
80103
const hiddenAgents = useQuery(agentListQueryOptions(true));
81104
const roster = [...(agents.data ?? []), ...(hiddenAgents.data ?? [])];
@@ -87,6 +110,11 @@ export function LearningPage() {
87110
);
88111
const delivery = useQuery(learningDeliveryQueryOptions(selected));
89112
const settings = draft ?? settingsQuery.data;
113+
// About what is saved, so it steps aside while an edit is in progress.
114+
const automaticNote =
115+
settingsQuery.data && !draft
116+
? automaticMessage(automatic.data, settingsQuery.data)
117+
: undefined;
90118
const save = useMutation({
91119
mutationFn: saveLearningSettings,
92120
onSuccess: async (next) => {
@@ -118,7 +146,7 @@ export function LearningPage() {
118146
>
119147
<PageSection
120148
title="Participation"
121-
description="Changes to container assignments apply to new threads. Existing threads keep their first assignment. Containers must already exist in the same Intelligence project."
149+
description="Changes to container assignments apply to new threads. Existing threads keep their first assignment. Containers must already exist in the same Intelligence project. Until anything is saved here, OpenBot assigns that project's openbot container automatically."
122150
>
123151
{settingsQuery.isPending && (
124152
<p role="status">Loading Learning settings…</p>
@@ -153,6 +181,11 @@ export function LearningPage() {
153181
change({ ...settings, defaultTarget })
154182
}
155183
/>
184+
{automaticNote && (
185+
<p role="status" className="text-sm">
186+
{automaticNote}
187+
</p>
188+
)}
156189
<p className="text-muted-foreground text-sm">
157190
An empty default leaves unmapped Bots out. IDs use 1–64 lowercase
158191
letters, numbers, and single hyphens. Leave revision empty to

‎app/tests/learning-admin.test.tsx‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,60 @@ test("active and hidden Bots both have participation controls", async () => {
130130
await view.findByLabelText("hidden-bot participation");
131131
expect(view.queryByLabelText("active-bot participation")).not.toBeNull();
132132
});
133+
134+
function serve(body: unknown) {
135+
global.fetch = Object.assign(
136+
async (input: RequestInfo | URL) => {
137+
const path = String(input);
138+
if (path.endsWith("/settings")) return Response.json(body);
139+
if (path.includes("/api/agents")) return Response.json({ agents: [] });
140+
return Response.json({ error: "unavailable" }, { status: 502 });
141+
},
142+
{ preconnect: originalFetch.preconnect },
143+
);
144+
}
145+
146+
test.each([
147+
[
148+
{ enabled: true, defaultTarget: null, agents: {} },
149+
{ state: "absent", containerId: "openbot" },
150+
'This Intelligence project has no "openbot" Learning container yet',
151+
],
152+
[
153+
{ enabled: true, defaultTarget: { containerId: "openbot" }, agents: {} },
154+
{ state: "assigned", containerId: "openbot" },
155+
'OpenBot assigned this Intelligence project\'s "openbot" container automatically',
156+
],
157+
[
158+
{ enabled: true, defaultTarget: null, agents: {} },
159+
{ state: "not_entitled" },
160+
"plan does not include Learning",
161+
],
162+
] as const)(
163+
"the page says what automatic assignment found (%#)",
164+
async (settings, automatic, expected) => {
165+
serve({ settings, automatic });
166+
const view = page();
167+
await view.findByText(expected, { exact: false });
168+
},
169+
);
170+
171+
test("a deployment that chose its own container hears nothing about automatic assignment", async () => {
172+
serve({
173+
settings: {
174+
enabled: true,
175+
defaultTarget: { containerId: "support" },
176+
agents: {},
177+
},
178+
automatic: { state: "saved" },
179+
});
180+
const view = page();
181+
await view.findByLabelText("Default container ID");
182+
await waitFor(() =>
183+
expect(view.queryByText("Loading Learning settings…")).toBeNull(),
184+
);
185+
expect(view.queryByText("OpenBot assigned", { exact: false })).toBeNull();
186+
expect(
187+
view.queryByText("Learning container yet", { exact: false }),
188+
).toBeNull();
189+
});

‎charts/openbot/README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,11 @@ nobody can talk to.
344344
## Automatic Learning
345345
346346
Learning is on by default, and it needs a container in the Intelligence project before it collects or
347-
delivers anything. `config.learning.containerId` sets the initial default container
347+
delivers anything. With nothing saved in Admin and no `config.learning.containerId`, the server
348+
assigns the project's `openbot` container as the default by itself once it exists. The runtime key
349+
cannot create it, so create it once, with `bun scripts/setup-learning.ts` from a computer that can
350+
sign in to Intelligence, or in Intelligence itself. `config.learning.autoAssign: false` turns the
351+
automatic assignment off. `config.learning.containerId` sets the initial default container
348352
(`CPK_INTELLIGENCE_LEARNING_CONTAINER_ID`) and `config.learning.revision` the skills revision
349353
(`CPK_INTELLIGENCE_SKILLS_REVISION`). Both are only starting values: settings saved in Admin →
350354
Automatic Learning take precedence, including switching it off. See

‎charts/openbot/templates/_helpers.tpl‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ and in whatever holds the release, which is not where `KEY_ENCRYPTION_KEY` belon
336336
- name: CPK_INTELLIGENCE_SKILLS_REVISION
337337
value: {{ . | quote }}
338338
{{- end }}
339+
{{- /* Absent, as it is under `--reuse-values`, means on: only an explicit false is written. */}}
340+
{{- if eq (toString $learning.autoAssign) "false" }}
341+
- name: OPENBOT_LEARNING_AUTO_ASSIGN
342+
value: "false"
343+
{{- end }}
339344
- name: INTELLIGENCE_API_URL
340345
value: {{ .Values.config.intelligence.apiUrl | quote }}
341346
- name: INTELLIGENCE_GATEWAY_WS_URL

‎charts/openbot/values.yaml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,15 @@ config:
161161
gatewayWsUrl: ""
162162
# The two secret halves live under `secrets` below, never here.
163163
# Learning defaults on; collection and delivery require an existing, assigned container.
164-
# Fresh self-hosted setup: run scripts/setup-learning.ts on a computer with Chrome/Edge access
165-
# to Intelligence, then transfer its runtime key through your secret manager and use openbot here.
166-
# Existing key-only installs create it through authenticated Intelligence administration first.
164+
# With nothing saved in Admin and no containerId below, the server assigns the project's openbot
165+
# container on its own once it exists; the runtime key cannot create it. Create it once with
166+
# scripts/setup-learning.ts (or in Intelligence), from a computer that can sign in to Intelligence.
167167
# Optional initial defaults; saved Admin → Automatic Learning settings take precedence, including off.
168168
learning:
169169
containerId: ""
170170
revision: ""
171+
# false stops the automatic assignment (OPENBOT_LEARNING_AUTO_ASSIGN=false).
172+
autoAssign: true
171173
# How far one Bot may hand work to another.
172174
#
173175
# A Bot addressing another is a grant an administrator makes, and these are the ceilings on what

0 commit comments

Comments
 (0)