mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 21:27:41 +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}")
|
||||
|
||||
Reference in New Issue
Block a user