mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-23 05:02:21 +02:00
117 lines
7.0 KiB
Python
117 lines
7.0 KiB
Python
"""Turn the local scan + app picks into a personalized greeting and starters.
|
|
|
|
One cheap aux call on whatever lane the user just connected; every failure path
|
|
returns the static fallback so the reveal can never be an error card.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from typing import List, Optional
|
|
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.agents.core.aux_llm import aux_max_tokens_for, safe_resp_text
|
|
from backend.apps.onboarding.models import PrepRequest, PrepResponse
|
|
from backend.apps.settings.models import AppSettings, PersonalizedAutomation, PersonalizedStarter
|
|
|
|
VALID_CADENCE = {"daily", "weekday", "weekly"}
|
|
|
|
FALLBACK_STARTERS: List[PersonalizedStarter] = [
|
|
PersonalizedStarter(title="Clean up Downloads", prompt="Sort my Downloads folder into tidy subfolders. Show me the plan before moving anything."),
|
|
PersonalizedStarter(title="Research something", prompt="Research the best noise-cancelling headphones under $300 and give me a comparison table."),
|
|
PersonalizedStarter(title="Build a tiny app", prompt="Build me a simple habit tracker app I can use right now."),
|
|
PersonalizedStarter(title="Plan a trip", prompt="Plan a 3-day weekend trip itinerary and turn it into a printable page."),
|
|
]
|
|
|
|
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, "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, 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 "
|
|
"(inspect folders, cross-reference, produce one report); it may create ONE new report file but must never "
|
|
"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. 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 "
|
|
"'daily', 'weekday', or 'weekly'. Automations must be safe to run unattended (never delete without review). "
|
|
"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."
|
|
)
|
|
|
|
|
|
@typechecked
|
|
def parse_prep(text: str) -> Optional[PrepResponse]:
|
|
match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
if not match:
|
|
return None
|
|
try:
|
|
data = json.loads(match.group(0))
|
|
starters = [
|
|
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()
|
|
]
|
|
if not starters:
|
|
return None
|
|
automations = [
|
|
PersonalizedAutomation(
|
|
title=str(a.get("title", "")).strip(),
|
|
prompt=str(a.get("prompt", "")).strip(),
|
|
cadence=(str(a.get("cadence", "weekly")).strip().lower() if str(a.get("cadence", "")).strip().lower() in VALID_CADENCE else "weekly"),
|
|
)
|
|
for a in data.get("automations", [])
|
|
if isinstance(a, dict) and str(a.get("title", "")).strip() and str(a.get("prompt", "")).strip()
|
|
]
|
|
return PrepResponse(
|
|
greeting=str(data.get("greeting", "")).strip(),
|
|
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:
|
|
return None
|
|
|
|
|
|
@typechecked
|
|
async def build_prep(settings: AppSettings, request: PrepRequest) -> PrepResponse:
|
|
facts = request.model_dump()
|
|
try:
|
|
from backend.apps.agents.providers.registry import resolve_aux_model
|
|
from backend.apps.settings.credentials import get_anthropic_client_for_model
|
|
|
|
aux_model, _ = await resolve_aux_model(settings, preferred_tier="haiku")
|
|
client = get_anthropic_client_for_model(settings, aux_model)
|
|
resp = await client.messages.create(
|
|
model=aux_model,
|
|
max_tokens=aux_max_tokens_for(aux_model, base=1100),
|
|
system=P_SYSTEM,
|
|
messages=[{"role": "user", "content": json.dumps(facts)}],
|
|
)
|
|
parsed = parse_prep(safe_resp_text(resp))
|
|
if parsed is not None:
|
|
return parsed
|
|
except Exception:
|
|
pass
|
|
return PrepResponse(greeting="", starters=list(FALLBACK_STARTERS))
|