agentkernel_sdk/
browser.rs1use base64::Engine;
7
8use crate::client::AgentKernel;
9use crate::error::{Error, Result};
10use crate::types::PageResult;
11
12const GOTO_SCRIPT: &str = r#"
17import asyncio, json, sys
18from playwright.async_api import async_playwright
19async def main():
20 url = sys.argv[1]
21 async with async_playwright() as p:
22 b = await p.chromium.launch()
23 page = await b.new_page()
24 await page.goto(url, timeout=30000)
25 title = await page.title()
26 url_final = page.url
27 text = await page.evaluate("() => document.body.innerText.slice(0, 8000)")
28 links = await page.evaluate('''() =>
29 Array.from(document.querySelectorAll('a[href]'))
30 .slice(0, 50)
31 .map(a => ({text: a.textContent.trim(), href: a.href}))
32 .filter(l => l.href.startsWith("http"))
33 ''')
34 print(json.dumps({"title": title, "url": url_final, "text": text, "links": links}))
35 await b.close()
36asyncio.run(main())
37"#;
38
39const SCREENSHOT_SCRIPT: &str = r#"
40import asyncio, base64, json, sys
41from playwright.async_api import async_playwright
42async def main():
43 url = sys.argv[1]
44 async with async_playwright() as p:
45 b = await p.chromium.launch()
46 page = await b.new_page()
47 await page.goto(url, timeout=30000)
48 data = await page.screenshot()
49 print(base64.b64encode(data).decode())
50 await b.close()
51asyncio.run(main())
52"#;
53
54const EVALUATE_SCRIPT: &str = r#"
55import asyncio, json, sys
56from playwright.async_api import async_playwright
57async def main():
58 url = sys.argv[1]
59 expr = sys.argv[2]
60 async with async_playwright() as p:
61 b = await p.chromium.launch()
62 page = await b.new_page()
63 await page.goto(url, timeout=30000)
64 result = await page.evaluate(expr)
65 print(json.dumps(result))
66 await b.close()
67asyncio.run(main())
68"#;
69
70pub const BROWSER_SETUP_CMD: &[&str] = &[
72 "sh",
73 "-c",
74 "pip install -q playwright && playwright install --with-deps chromium",
75];
76
77pub struct BrowserSession {
96 name: String,
98 client: AgentKernel,
100 removed: bool,
102 last_url: Option<String>,
104}
105
106impl BrowserSession {
107 pub(crate) fn new(name: String, client: AgentKernel) -> Self {
112 Self {
113 name,
114 client,
115 removed: false,
116 last_url: None,
117 }
118 }
119
120 pub fn name(&self) -> &str {
122 &self.name
123 }
124
125 pub async fn goto(&mut self, url: &str) -> Result<PageResult> {
127 let output = self
128 .client
129 .exec_in_sandbox(&self.name, &["python3", "-c", GOTO_SCRIPT, url], None)
130 .await?;
131 self.last_url = Some(url.to_string());
132 let result: PageResult = serde_json::from_str(&output.output)?;
133 Ok(result)
134 }
135
136 pub async fn screenshot(&self, url: Option<&str>) -> Result<Vec<u8>> {
140 let target = url
141 .map(String::from)
142 .or_else(|| self.last_url.clone())
143 .ok_or_else(|| {
144 Error::Validation("No URL specified and no previous goto() call".to_string())
145 })?;
146 let output = self
147 .client
148 .exec_in_sandbox(
149 &self.name,
150 &["python3", "-c", SCREENSHOT_SCRIPT, &target],
151 None,
152 )
153 .await?;
154 let bytes = base64::engine::general_purpose::STANDARD
155 .decode(output.output.trim())
156 .map_err(|e| Error::Server(format!("base64 decode failed: {e}")))?;
157 Ok(bytes)
158 }
159
160 pub async fn evaluate(&self, expression: &str, url: Option<&str>) -> Result<serde_json::Value> {
164 let target = url
165 .map(String::from)
166 .or_else(|| self.last_url.clone())
167 .ok_or_else(|| {
168 Error::Validation("No URL specified and no previous goto() call".to_string())
169 })?;
170 let output = self
171 .client
172 .exec_in_sandbox(
173 &self.name,
174 &["python3", "-c", EVALUATE_SCRIPT, &target, expression],
175 None,
176 )
177 .await?;
178 let value: serde_json::Value = serde_json::from_str(&output.output)?;
179 Ok(value)
180 }
181
182 pub async fn remove(&mut self) -> Result<()> {
184 if self.removed {
185 return Ok(());
186 }
187 self.removed = true;
188 self.client.remove_sandbox(&self.name).await
189 }
190}