mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 02:07:45 +02:00
[eric] browser: surface learned playbook in-chat + REST list/forget so site memory is visible and clearable
This commit is contained in:
@@ -238,6 +238,32 @@ async def get_browser_agent_children(session_id: str):
|
||||
children = agent_manager.get_browser_agent_children(session_id)
|
||||
return {"sessions": children}
|
||||
|
||||
@agents.router.get("/browser-memory")
|
||||
async def list_browser_memory():
|
||||
"""Everything the browser agent has learned, per site, so the user can see it
|
||||
and clear it: tier-1 skills (replayable shortcuts) + tier-2 playbook (strategy
|
||||
text). Read-only; pure introspection."""
|
||||
from backend.apps.agents.browser import browser_playbook, browser_skills
|
||||
sites: dict[str, dict] = {}
|
||||
for entry in browser_playbook.list_hosts():
|
||||
sites.setdefault(entry["host"], {"host": entry["host"], "skills": [], "strategy": []})
|
||||
sites[entry["host"]]["strategy"] = entry["bullets"]
|
||||
sites[entry["host"]]["updated_at"] = entry.get("updated_at", 0)
|
||||
for host in list(sites.keys()):
|
||||
sites[host]["skills"] = browser_skills.list_skills(host)
|
||||
return {"sites": sorted(sites.values(), key=lambda s: -s.get("updated_at", 0))}
|
||||
|
||||
|
||||
@agents.router.delete("/browser-memory/{host}")
|
||||
async def forget_browser_memory(host: str):
|
||||
"""Clear what the browser agent learned about one site (strategy + skills); it
|
||||
re-learns on the next successful run."""
|
||||
from backend.apps.agents.browser import browser_playbook, browser_skills
|
||||
forgot_strategy = browser_playbook.forget(host)
|
||||
forgot_skills = browser_skills.forget_host(host)
|
||||
return {"ok": True, "host": host, "forgot_strategy": forgot_strategy, "forgot_skills": forgot_skills}
|
||||
|
||||
|
||||
@agents.router.post("/sessions/{session_id}/resume")
|
||||
async def resume_session(session_id: str):
|
||||
try:
|
||||
|
||||
@@ -773,6 +773,8 @@ async def run_browser_agent(
|
||||
cur_host = browser_skills.host_of(last_seen_url) or replay_host
|
||||
if tu.name == "BrowserListSkills":
|
||||
skills = browser_skills.list_skills(cur_host) if cur_host else []
|
||||
playbook = browser_playbook.get_playbook(cur_host) if cur_host else []
|
||||
parts = []
|
||||
if skills:
|
||||
_tag = {"trusted": "proven", "probation": "unproven", "quarantine": "disabled"}
|
||||
def _fmt_skill(s):
|
||||
@@ -780,10 +782,10 @@ async def run_browser_agent(
|
||||
if s.get("builds_on"):
|
||||
line += f", builds on {len(s['builds_on'])} other shortcut(s)"
|
||||
return line + ")"
|
||||
lines = "\n".join(_fmt_skill(s) for s in skills[:20])
|
||||
meta_text = f"Learned shortcuts for {cur_host}:\n{lines}"
|
||||
else:
|
||||
meta_text = f"No learned shortcuts for {cur_host or 'this site'} yet."
|
||||
parts.append(f"Learned shortcuts for {cur_host}:\n" + "\n".join(_fmt_skill(s) for s in skills[:20]))
|
||||
if playbook:
|
||||
parts.append(f"Strategy I've learned about {cur_host}:\n" + "\n".join(f"- {b}" for b in playbook))
|
||||
meta_text = "\n\n".join(parts) if parts else f"Nothing learned for {cur_host or 'this site'} yet."
|
||||
else:
|
||||
target = tu.input.get("task", "")
|
||||
ok = browser_skills.deprecate_skill(cur_host, target) if cur_host else False
|
||||
|
||||
@@ -691,6 +691,28 @@ def deprecate_skill(host: str, task: str) -> bool:
|
||||
return removed
|
||||
|
||||
|
||||
def forget_host(host: str) -> int:
|
||||
"""Remove ALL learned skills for a host (memory + disk). For the user-facing
|
||||
'forget this site' control. Returns how many were removed."""
|
||||
if not host:
|
||||
return 0
|
||||
n = 0
|
||||
for sig in list(_host_skills(host).keys()):
|
||||
removed = _skills.pop(_key(host, sig), None) is not None
|
||||
path = _skill_path(host, sig)
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
removed = True
|
||||
except Exception:
|
||||
pass
|
||||
if removed:
|
||||
n += 1
|
||||
if n:
|
||||
logger.info(f"[browser-skills] forgot all {n} skill(s) for {host}")
|
||||
return n
|
||||
|
||||
|
||||
def clear(wipe_disk: bool = False) -> None:
|
||||
"""Clear the in-memory cache. With wipe_disk, also remove persisted files
|
||||
in the current skills dir (used by tests for isolation)."""
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""The browser-memory UX surface: list + forget endpoints.
|
||||
|
||||
Calls the real route handlers directly (thin wrappers over the already-tested
|
||||
skill + playbook stores) so the user-facing 'see what it learned / forget it'
|
||||
controls are proven, including that forget actually clears both tiers.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from backend.apps.agents import agents as agents_mod
|
||||
from backend.apps.agents.browser import browser_playbook as pb
|
||||
from backend.apps.agents.browser import browser_skills as sk
|
||||
|
||||
|
||||
def _seed_skill(host, task):
|
||||
sk.record_skill(host, task, [
|
||||
{"tool": "BrowserClickIndex", "input": {}, "ok": True,
|
||||
"clicked_role": "button", "clicked_name": "Go"},
|
||||
])
|
||||
|
||||
|
||||
async def _seed_strategy(host, *bullets):
|
||||
class _Blk:
|
||||
def __init__(self, t): self.text = t
|
||||
|
||||
class _Resp:
|
||||
def __init__(self, t): self.content = [_Blk(t)]
|
||||
|
||||
class _Aux:
|
||||
def __init__(self): self.messages = self
|
||||
async def create(self, **kw):
|
||||
return _Resp(json.dumps({"playbook": list(bullets)}))
|
||||
await pb.distill_and_store(host, "t", "m", "s", _Aux(), "aux")
|
||||
|
||||
|
||||
def test_list_browser_memory_groups_skills_and_strategy_by_site():
|
||||
sk.clear(); pb.clear(wipe_disk=True)
|
||||
_seed_skill("shop.com", "search now")
|
||||
asyncio.run(_seed_strategy("shop.com", "use the search box at the top"))
|
||||
asyncio.run(_seed_strategy("docs.com", "share lives behind the blue button"))
|
||||
|
||||
out = asyncio.run(agents_mod.list_browser_memory())
|
||||
sites = {s["host"]: s for s in out["sites"]}
|
||||
assert set(sites) == {"shop.com", "docs.com"}
|
||||
assert sites["shop.com"]["strategy"] == ["use the search box at the top"]
|
||||
assert len(sites["shop.com"]["skills"]) == 1
|
||||
assert sites["docs.com"]["strategy"] == ["share lives behind the blue button"]
|
||||
|
||||
|
||||
def test_forget_clears_both_tiers_for_a_site():
|
||||
sk.clear(); pb.clear(wipe_disk=True)
|
||||
_seed_skill("gone.com", "do it now")
|
||||
asyncio.run(_seed_strategy("gone.com", "a strategy bullet"))
|
||||
# sanity: present
|
||||
assert pb.get_playbook("gone.com") and sk.list_skills("gone.com")
|
||||
|
||||
res = asyncio.run(agents_mod.forget_browser_memory("gone.com"))
|
||||
assert res["ok"] and res["forgot_strategy"] is True and res["forgot_skills"] >= 1
|
||||
# both tiers actually cleared
|
||||
assert pb.get_playbook("gone.com") == []
|
||||
assert sk.list_skills("gone.com") == []
|
||||
# the site no longer appears in the listing
|
||||
out = asyncio.run(agents_mod.list_browser_memory())
|
||||
assert "gone.com" not in {s["host"] for s in out["sites"]}
|
||||
|
||||
|
||||
def test_forget_unknown_site_is_harmless():
|
||||
sk.clear(); pb.clear(wipe_disk=True)
|
||||
res = asyncio.run(agents_mod.forget_browser_memory("never.com"))
|
||||
assert res["ok"] and res["forgot_strategy"] is False and res["forgot_skills"] == 0
|
||||
Reference in New Issue
Block a user