mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-09 19:27:45 +02:00
[eric] onboarding: reveal shows what it saw and why it acted (per-job personalized reasons)
This commit is contained in:
@@ -56,4 +56,5 @@ class PrepResponse(BaseModel):
|
||||
starters: List[PersonalizedStarter] = Field(default_factory=list)
|
||||
app_title: str = ""
|
||||
app_prompt: str = ""
|
||||
app_reason: str = ""
|
||||
automations: List[PersonalizedAutomation] = Field(default_factory=list)
|
||||
|
||||
@@ -27,7 +27,7 @@ P_SYSTEM = (
|
||||
"You write first-run starter tasks for OpenSwarm, a desktop AI agent platform that can "
|
||||
"organize local files, browse the web in a real browser, build small apps, and run agents in parallel. "
|
||||
"Given facts about the user's machine and the apps they picked, respond with STRICT JSON only: "
|
||||
'{"greeting": string, "starters": [{"title": string, "prompt": string}], "app_title": string, "app_prompt": string, "automations": [{"title": string, "prompt": string, "cadence": "daily"|"weekday"|"weekly"}]}. '
|
||||
'{"greeting": string, "starters": [{"title": string, "prompt": string, "reason": string}], "app_title": string, "app_prompt": string, "app_reason": string, "automations": [{"title": string, "prompt": string, "cadence": "daily"|"weekday"|"weekly"}]}. '
|
||||
"First, silently infer a short profile of this user. If usage_summary is present it is the STRONGEST signal (it is "
|
||||
"what they actually ask their AI about and facts their AI remembers about them); weight it above everything else, "
|
||||
"then apps, folders, plan tier, email domain. Tune every task and the personal app to that profile; do not output the profile. "
|
||||
@@ -38,9 +38,13 @@ P_SYSTEM = (
|
||||
"modify or delete existing files, because it may be run automatically on the user's behalf. Every starter "
|
||||
"must produce a tangible result the user can see (a sorted "
|
||||
"folder, a report, a working page); never propose setup, documentation of preferences, or planning-only tasks. "
|
||||
"Each starter's 'reason' is ONE short standalone clause (max 12 words, no leading 'because') naming the SPECIFIC "
|
||||
"real thing you observed (a folder, a file count, a picked app, a usage fact) that makes this task useful for THIS "
|
||||
"user; it must be grounded in the input facts, never invented, and read like a person pointing at what they saw. "
|
||||
"Also design ONE small personal app for this user: app_title is 2-4 words, app_prompt starts with 'Build me' and "
|
||||
"describes a small, immediately useful single-page app tailored to the profile (their files, habits, or picked "
|
||||
"apps), self-contained with no accounts or API keys. "
|
||||
"apps), self-contained with no accounts or API keys. app_reason follows the same one-clause grounded-observation "
|
||||
"rule as a starter reason. "
|
||||
"Also propose 2-3 automations: recurring routines worth running on a schedule for THIS user, drawn from their "
|
||||
"profile and habits (for example a daily morning brief, a weekly folder cleanup, a weekday summary of their "
|
||||
"connected apps). Each automation title is 2-4 words, prompt is one runnable instruction, cadence is exactly "
|
||||
@@ -58,7 +62,7 @@ def parse_prep(text: str) -> Optional[PrepResponse]:
|
||||
try:
|
||||
data = json.loads(match.group(0))
|
||||
starters = [
|
||||
PersonalizedStarter(title=str(s.get("title", "")).strip(), prompt=str(s.get("prompt", "")).strip())
|
||||
PersonalizedStarter(title=str(s.get("title", "")).strip(), prompt=str(s.get("prompt", "")).strip(), reason=str(s.get("reason", "")).strip())
|
||||
for s in data.get("starters", [])
|
||||
if isinstance(s, dict) and str(s.get("title", "")).strip() and str(s.get("prompt", "")).strip()
|
||||
]
|
||||
@@ -78,6 +82,7 @@ def parse_prep(text: str) -> Optional[PrepResponse]:
|
||||
starters=starters[:4],
|
||||
app_title=str(data.get("app_title", "")).strip(),
|
||||
app_prompt=str(data.get("app_prompt", "")).strip(),
|
||||
app_reason=str(data.get("app_reason", "")).strip(),
|
||||
automations=automations[:3],
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -111,6 +111,8 @@ class CustomProvider(BaseModel):
|
||||
class PersonalizedStarter(BaseModel):
|
||||
title: str
|
||||
prompt: str
|
||||
# One short clause tying this task to something real we saw about the user; the reveal shows it off.
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class PersonalizedAutomation(BaseModel):
|
||||
|
||||
@@ -84,10 +84,24 @@ def test_parse_prep_strict_and_lenient():
|
||||
assert parsed is not None
|
||||
assert parsed.greeting == "Hey!"
|
||||
assert [s.title for s in parsed.starters] == ["A", "B"]
|
||||
# Reason is optional; missing reason defaults to empty, never drops the starter.
|
||||
assert parsed.starters[0].reason == ""
|
||||
assert parse_prep("no json here") is None
|
||||
assert parse_prep('{"greeting": "hi", "starters": []}') is None
|
||||
|
||||
|
||||
def test_parse_prep_carries_reasons():
|
||||
rich = (
|
||||
'{"greeting": "Hey!", "app_title": "Lift Log", "app_prompt": "Build me a lifting tracker", '
|
||||
'"app_reason": "you plan lifts with ChatGPT daily", '
|
||||
'"starters": [{"title": "Audit Downloads", "prompt": "audit it", "reason": "1,305 files piling up there"}]}'
|
||||
)
|
||||
parsed = parse_prep(rich)
|
||||
assert parsed is not None
|
||||
assert parsed.starters[0].reason == "1,305 files piling up there"
|
||||
assert parsed.app_reason == "you plan lifts with ChatGPT daily"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_prep_fails_open_without_provider(monkeypatch):
|
||||
async def boom(*args, **kwargs):
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface PrepResponse {
|
||||
starters: PersonalizedStarter[];
|
||||
app_title: string;
|
||||
app_prompt: string;
|
||||
app_reason: string;
|
||||
automations: PersonalizedAutomation[];
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useOnboardingV3Pipeline() {
|
||||
}, []);
|
||||
|
||||
// Fire one real background agent; the session exists in redux without a card until the reveal composes the canvas.
|
||||
const launchJob = useCallback((title: string, prompt: string, kind: 'audit' | 'app') => {
|
||||
const launchJob = useCallback((title: string, prompt: string, kind: 'audit' | 'app', reason: string) => {
|
||||
const { model: liveModel } = launchCtxRef.current;
|
||||
const dashboardId = getLastDashboardId() ?? undefined;
|
||||
const config: AgentConfig = { name: title, model: liveModel, mode: 'agent', dashboard_id: dashboardId };
|
||||
@@ -79,7 +79,7 @@ export function useOnboardingV3Pipeline() {
|
||||
void dispatch(launchAndSendFirstMessage({ draftId, config, prompt, mode: 'agent', model: liveModel }))
|
||||
.then((action) => {
|
||||
if (launchAndSendFirstMessage.fulfilled.match(action)) {
|
||||
dispatch(addPreppedJob({ sessionId: action.payload.session.id, title, kind }));
|
||||
dispatch(addPreppedJob({ sessionId: action.payload.session.id, title, kind, reason }));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -96,8 +96,8 @@ export function useOnboardingV3Pipeline() {
|
||||
void prepRef.current.then((prep) => {
|
||||
if (launchedRef.current || !prep || !prep.greeting || !launchCtxRef.current.connected) return;
|
||||
launchedRef.current = true;
|
||||
if (prep.starters.length > 0) launchJob(prep.starters[0].title, prep.starters[0].prompt, 'audit');
|
||||
if (prep.app_title && prep.app_prompt) launchJob(prep.app_title, prep.app_prompt, 'app');
|
||||
if (prep.starters.length > 0) launchJob(prep.starters[0].title, prep.starters[0].prompt, 'audit', prep.starters[0].reason ?? '');
|
||||
if (prep.app_title && prep.app_prompt) launchJob(prep.app_title, prep.app_prompt, 'app', prep.app_reason ?? '');
|
||||
});
|
||||
}, [launchJob]);
|
||||
|
||||
|
||||
@@ -34,14 +34,15 @@ export function useOnboardingRevealSeed({ isActive, dashboardId, expandedSession
|
||||
const cy = (vr.height / 2 - cs.panY) / cs.zoom;
|
||||
const audit = prepped.find((j) => j.kind === 'audit');
|
||||
const app = prepped.find((j) => j.kind === 'app');
|
||||
const lines: string[] = ['Here is what I set up for you.'];
|
||||
if (scanSummary) lines.push(`Looked around: ${scanSummary}.`);
|
||||
const jobLine = (j: typeof prepped[number]) => (j.reason ? `- ${j.title}: ${j.reason}` : `- ${j.title}`);
|
||||
const lines: string[] = ['While you were setting up, I got a head start.'];
|
||||
if (scanSummary) lines.push(`I looked around your Mac and saw ${scanSummary}.`);
|
||||
const cards: string[] = [];
|
||||
if (audit) cards.push(`- ${audit.title} (running now, on the left)`);
|
||||
if (app) cards.push(`- ${app.title} (building now)`);
|
||||
if (cards.length > 0) lines.push(`Working on:\n${cards.join('\n')}`);
|
||||
if (starters.length > 0) lines.push(`Ready when you are:\n${starters.slice(audit ? 1 : 0).map((s) => `- ${s.title}`).join('\n')}`);
|
||||
lines.push('Nothing is saved or deleted without you. Keep or discard anytime.');
|
||||
if (audit) cards.push(jobLine(audit));
|
||||
if (app) cards.push(jobLine(app));
|
||||
if (cards.length > 0) lines.push(`So here is what I already have going (cards on your left):\n${cards.join('\n')}`);
|
||||
if (starters.length > 0) lines.push(`Ready whenever you want:\n${starters.slice(audit ? 1 : 0).map((s) => `- ${s.title}`).join('\n')}`);
|
||||
lines.push('Nothing is saved or deleted without you. Keep it going or clear it anytime.');
|
||||
dispatch(addNote({ x: cx + DEFAULT_CARD_W / 2 + 48, y: cy - 140, color: 'yellow', content: lines.join('\n\n') }));
|
||||
prepped.forEach((job, i) => {
|
||||
dispatch(placeCard({
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface PreppedJob {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
kind: 'audit' | 'app';
|
||||
/** The one-clause "why we started this for you", shown in the reveal note. */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface OnboardingV3State {
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface AppSettings {
|
||||
export interface PersonalizedStarter {
|
||||
title: string;
|
||||
prompt: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface PersonalizedAutomation {
|
||||
|
||||
Reference in New Issue
Block a user