[eric] browser: surface learned playbook in-chat + REST list/forget so site memory is visible and clearable

This commit is contained in:
ciregenz
2026-06-02 23:16:22 -07:00
parent fe082c492c
commit f187ec7c6e
4 changed files with 125 additions and 4 deletions
+26
View File
@@ -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:
+6 -4
View File
@@ -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