"""Artifacts: the agent makes and revises a web page from a cell, both clients learn of it from the cell's trace, its pages are served through the frame routes with the bridge, the page's storage and failures reach the agent, and the artifact outlives its session. See robot-docs/artifacts.md.""" import json import re import threading import time import unittest import urllib.error from harness import Albedo, Provider, prompt_text, python, text PAGE = ( "
hé
" ) # Each prompt is the cell the scripted model runs for it; `{id}` is the # artifact the test made first. CELLS = { "make": "a = await artifacts.create('release timeline', files={'index.html': PAGE, 'app.js': 'console.log(1)', 'img/dot.bin': bytes(range(256)), 'notes/a note.txt': 'hi'})\nprint(json.dumps(a))", "revise script": "a = await artifacts.update(ID, files={'app.js': 'console.log(2)', 'img/dot.bin': None})\nprint(json.dumps(a))", "revise page": "a = await artifacts.update(ID, 'second page
', title='release timeline, revised')\nprint(json.dumps({**a, 'page': await artifacts.read(ID), 'first': (await artifacts.get(ID, 1)).files}))", "read back": "print(json.dumps({'state': await artifacts.state(ID), 'errors': await artifacts.errors(ID), 'mine': [a.id for a in await artifacts.list(session='current')]}))", "make plain": "a = await artifacts.create('counter', files={'index.html': 'one
', 'app.js': '1'})\nprint(json.dumps(a))", "update plain": "a = await artifacts.update(ID, files={'index.html': 'two
', 'app.js': '2'})\nprint(json.dumps(a))", "refused": """for make in ( lambda: artifacts.create('built', files={'index.html': ''}), lambda: artifacts.create('x' * 201, 'hi
'), lambda: artifacts.update(ID, files={'gone.js': None}), ): try: await make() except Exception as error: print(type(error).__name__, error)""", } class ArtifactsTest(unittest.TestCase): def setUp(self): self.artifact = "" def script(request): last = request["messages"][-1] prompt = prompt_text(request) if last.get("role") == "user" and prompt in CELLS: prelude = f"import json\nPAGE = {PAGE!r}\nID = {self.artifact!r}\n" return python(prelude + CELLS[prompt]) return text("done") self.provider = Provider(script) self.addCleanup(self.provider.close) self.app = Albedo(self.provider) self.app.__enter__() self.addCleanup(self.app.__exit__, None, None, None) def run_cell(self, session, prompt): """The cell's printed output and the trace it left.""" self.app.prompt(session, prompt).close() self.app.idle(session) tool = [ entry for entry in self.app.history(session)["items"] if entry["kind"] == "tool_result" ][-1] result = next( json.loads(part["value"]) for part in tool["content"] if part["kind"] == "json" and part["field"] == "result" ) self.assertEqual(result["status"], "ok", result) trace = next( (part["trace"] for part in tool["content"] if part["kind"] == "trace"), {"activities": []}, ) return result["output"].strip(), trace def get(self, path, headers=None): with self.app.api(path, headers=headers) as response: return response.status, dict(response.getheaders()), response.read() def refused(self, path, method=None, body=None): try: self.app.api(path, body, method=method).close() except urllib.error.HTTPError as error: return error.code self.fail(f"{path} was served") def post_text(self, path, body): connection = self.app.daemon._connection() connection.request( "POST", path, body.encode(), { "Authorization": "Bearer " + connection.token, "Content-Type": "text/plain;charset=UTF-8", }, ) response = connection.getresponse() response.read() return response.status def test_an_artifact_is_made_revised_served_and_kept(self): session = self.app.session() output, trace = self.run_cell(session, "make") made = json.loads(output) self.artifact = made["id"] self.assertRegex(self.artifact, r"^[a-z0-9]{12}$") self.assertEqual(made["version"], 1) self.assertEqual(made["title"], "release timeline") self.assertEqual( [a for a in trace["activities"] if a["kind"] == "artifact"], [ { "kind": "artifact", "target": "release timeline", "artifact": {"id": self.artifact, "version": 1}, } ], ) status, _, body = self.get(f"/extensions/artifacts/items?session={session}") listed = json.loads(body)["items"] self.assertEqual([item["id"] for item in listed], [self.artifact]) item = listed[0] self.assertRegex(item["key"], r"^[0-9a-f]{32}$") self.assertFalse(item["session_deleted"]) self.assertEqual(item["entry"], "index.html") frame = f"/extensions/artifacts/frame/{self.artifact}.{item['key']}" # The entry page gets the bridge right after its head opens, in the # page's own bytes; other files are served as they are, for good. status, headers, page = self.get(frame + "/1/") self.assertEqual(status, 200) self.assertTrue(headers["content-type"].startswith("text/html")) self.assertTrue( page.startswith( b"