mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-21 12:12:22 +02:00
[eric] onboarding: sharpen the scan-driven reveal (surface telling apps, insight-led greeting)
This commit is contained in:
@@ -21,6 +21,18 @@ SCAN_FOLDERS = ("Downloads", "Desktop", "Documents")
|
||||
REPO_PARENT_CANDIDATES = ("dev", "code", "projects", "src", "repos", "Documents/GitHub")
|
||||
SCREENSHOT_PREFIXES = ("screenshot", "screen shot", "screen recording")
|
||||
|
||||
# The high-signal tools that actually tell prep who this person is (an IDE, a design app, a DAW) so the greeting can lead with a confident read instead of drowning in Calculator + System Settings. Matched by exact name or "name " prefix so "Arc" never trips on "Search".
|
||||
NOTABLE_APP_KEYWORDS = (
|
||||
"xcode", "visual studio code", "cursor", "zed", "sublime text", "intellij idea",
|
||||
"pycharm", "webstorm", "android studio", "docker", "orbstack", "postman", "iterm",
|
||||
"warp", "ghostty", "tableplus", "github desktop", "figma", "sketch",
|
||||
"adobe photoshop", "adobe illustrator", "adobe xd", "affinity", "framer", "blender",
|
||||
"cinema 4d", "final cut pro", "davinci resolve", "adobe premiere pro",
|
||||
"adobe after effects", "screenflow", "capcut", "obs", "logic pro", "ableton live",
|
||||
"fl studio", "garageband", "unity", "unreal engine", "godot", "notion", "obsidian",
|
||||
"ulysses", "scrivener", "craft", "linear", "tableau", "rstudio", "ollama", "lm studio", "arc",
|
||||
)
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_list_apps() -> List[str]:
|
||||
@@ -66,6 +78,16 @@ def p_summarize_folder(folder: Path) -> FolderSummary:
|
||||
return summary
|
||||
|
||||
|
||||
@typechecked
|
||||
def detect_signal_apps(apps: List[str]) -> List[str]:
|
||||
out: List[str] = []
|
||||
for name in apps:
|
||||
low = name.lower()
|
||||
if any(low == kw or low.startswith(kw + " ") for kw in NOTABLE_APP_KEYWORDS):
|
||||
out.append(name)
|
||||
return out[:20]
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_count_git_repos(home: Path) -> int:
|
||||
count = 0
|
||||
@@ -90,8 +112,10 @@ def p_count_git_repos(home: Path) -> int:
|
||||
@typechecked
|
||||
def run_local_scan(home: Path) -> ScanResult:
|
||||
folders = [p_summarize_folder(home / name) for name in SCAN_FOLDERS]
|
||||
apps = p_list_apps()
|
||||
return ScanResult(
|
||||
apps=p_list_apps(),
|
||||
apps=apps,
|
||||
signal_apps=detect_signal_apps(apps),
|
||||
folders=folders,
|
||||
git_repo_count=p_count_git_repos(home),
|
||||
has_gitconfig=(home / ".gitconfig").is_file(),
|
||||
|
||||
@@ -35,6 +35,8 @@ class ScanResult(BaseModel):
|
||||
model_config = ConfigDict(validate_assignment=True)
|
||||
|
||||
apps: List[str] = Field(default_factory=list)
|
||||
# The high-signal subset of apps (IDEs, design/creative tools); the profile leans on these.
|
||||
signal_apps: List[str] = Field(default_factory=list)
|
||||
folders: List[FolderSummary] = Field(default_factory=list)
|
||||
git_repo_count: int = 0
|
||||
has_gitconfig: bool = False
|
||||
|
||||
@@ -28,9 +28,11 @@ P_SYSTEM = (
|
||||
"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, "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. "
|
||||
"First, silently infer a short, confident profile of this user: who they are and what they are working on. "
|
||||
"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 signal_apps (the high-signal tools they "
|
||||
"have installed, like an IDE, a design app, or a DAW: these reveal their craft), then folders, plan tier, email "
|
||||
"domain. Tune every task and the personal app to that profile; do not output the profile. "
|
||||
"Exactly 4 starters. Each title is 2-5 words. Each prompt is a concrete, safe, immediately runnable task "
|
||||
"referencing the user's real folders, file counts, or picked apps when possible; never invent facts, never "
|
||||
"propose deleting anything without review. The FIRST starter must be an audit sized for parallel sub-work "
|
||||
@@ -49,8 +51,10 @@ P_SYSTEM = (
|
||||
"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 "
|
||||
"'daily', 'weekday', or 'weekly'. Automations must be safe to run unattended (never delete without review). "
|
||||
"The greeting is one warm sentence that names 2-3 specific things "
|
||||
"found on the machine. Never use em-dashes or en-dashes anywhere. No markdown, no commentary, JSON only."
|
||||
"The greeting is one or two warm sentences: first say out loud, specifically and confidently, what this person is "
|
||||
"into or working on (grounded in usage_summary, signal_apps, and folders, for example 'Looks like you live in "
|
||||
"Xcode and ship iOS apps'), then name 2-3 concrete things you actually saw. Be specific, never generic, and never "
|
||||
"name boring system apps. Never use em-dashes or en-dashes anywhere. No markdown, no commentary, JSON only."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from backend.apps.onboarding.identity import build_identity, decode_jwt_payload
|
||||
from backend.apps.onboarding.local_scan import run_local_scan
|
||||
from backend.apps.onboarding.local_scan import detect_signal_apps, run_local_scan
|
||||
from backend.apps.onboarding.models import PrepRequest, ScanResult
|
||||
from backend.apps.onboarding.prep import FALLBACK_STARTERS, build_prep, parse_prep
|
||||
from backend.apps.settings.models import AppSettings
|
||||
@@ -78,6 +78,17 @@ def test_run_local_scan_counts_names_only(tmp_path: Path):
|
||||
assert "[user]" not in serialized
|
||||
|
||||
|
||||
def test_signal_apps_picks_tools_not_noise_and_avoids_substring_traps():
|
||||
apps = ["Calculator", "Xcode", "Visual Studio Code", "Figma", "Search", "System Settings", "Adobe Photoshop 2024", "Arc"]
|
||||
signal = detect_signal_apps(apps)
|
||||
assert "Xcode" in signal and "Visual Studio Code" in signal and "Figma" in signal
|
||||
assert "Adobe Photoshop 2024" in signal # prefix match on a versioned name
|
||||
assert "Arc" in signal
|
||||
# "Search" contains "arc" as a substring but must NOT be treated as the Arc browser.
|
||||
assert "Search" not in signal
|
||||
assert "Calculator" not in signal and "System Settings" not in signal
|
||||
|
||||
|
||||
def test_parse_prep_strict_and_lenient():
|
||||
good = '{"greeting": "Hey!", "starters": [{"title": "A", "prompt": "do a"}, {"title": "B", "prompt": "do b"}]}'
|
||||
parsed = parse_prep(f"Sure! Here you go: {good}")
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface FolderSummary {
|
||||
|
||||
export interface ScanResult {
|
||||
apps: string[];
|
||||
signal_apps: string[];
|
||||
folders: FolderSummary[];
|
||||
git_repo_count: number;
|
||||
has_gitconfig: boolean;
|
||||
@@ -67,14 +68,15 @@ export async function runPrep(scan: ScanResult | null, pickedApps: string[], ide
|
||||
}
|
||||
}
|
||||
|
||||
// One human-readable line about what the scan found, reused by the reveal note so the user can see exactly what informed their starters.
|
||||
// One human-readable line about what the scan found, reused by the reveal note so the user can see exactly what informed their starters. Leads with the telling apps (the sharp part), then repos + folder volume.
|
||||
export function summarizeScan(scan: ScanResult | null): string | null {
|
||||
if (!scan) return null;
|
||||
const parts: string[] = [];
|
||||
if (scan.signal_apps?.length) parts.push(scan.signal_apps.slice(0, 3).join(', '));
|
||||
if (scan.git_repo_count > 0) parts.push(`${scan.git_repo_count} git repo${scan.git_repo_count === 1 ? '' : 's'}`);
|
||||
for (const f of scan.folders) {
|
||||
if (f.screenshot_count > 20) parts.push(`${f.screenshot_count} screenshots on your ${f.name}`);
|
||||
else if (f.entry_count > 300) parts.push(`${f.entry_count} items in ${f.name}`);
|
||||
else if (f.entry_count > 300) parts.push(`${f.entry_count} files in ${f.name}`);
|
||||
}
|
||||
if (scan.git_repo_count > 0) parts.push(`${scan.git_repo_count} git repo${scan.git_repo_count === 1 ? '' : 's'}`);
|
||||
return parts.length > 0 ? parts.slice(0, 3).join(', ') : null;
|
||||
return parts.length > 0 ? parts.slice(0, 4).join(', ') : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user