Skip to content

Commit f4be095

Browse files
committed
bump to 0.3.0, add persistent browser examples
1 parent bb04eea commit f4be095

File tree

15 files changed

+742
-4
lines changed

15 files changed

+742
-4
lines changed

templates/python/browser-use/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"browser-use>=0.1.46",
9-
"kernel==0.2.0",
9+
"kernel==0.3.0",
1010
"langchain-openai>=0.3.11",
1111
"pydantic>=2.10.6",
1212
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Virtual Environment
24+
.env
25+
.venv
26+
env/
27+
venv/
28+
ENV/
29+
env.bak/
30+
venv.bak/
31+
activate/
32+
33+
# IDE
34+
.idea/
35+
.vscode/
36+
*.swp
37+
*.swo
38+
.project
39+
.pydevproject
40+
.settings/
41+
42+
# Testing
43+
.coverage
44+
htmlcov/
45+
.pytest_cache/
46+
.tox/
47+
.nox/
48+
coverage.xml
49+
*.cover
50+
.hypothesis/
51+
52+
# Logs
53+
*.log
54+
logs/
55+
56+
# OS
57+
.DS_Store
58+
Thumbs.db
59+
60+
# Misc
61+
.cache/
62+
.pytest_cache/
63+
.mypy_cache/
64+
.ruff_cache/
65+
.temp/
66+
.tmp/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Kernel Python Persistent Browser Example
2+
3+
This is a simple Kernel application that shows how to use a persistent browser.
4+
5+
The action reuses the same browser across invocations of the action, which lets it optimize for the case where the browser is already open and at the page of interest.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import kernel
2+
from kernel import Kernel
3+
from playwright.async_api import async_playwright
4+
from typing import TypedDict
5+
from urllib.parse import urlparse
6+
from datetime import datetime
7+
8+
client = Kernel()
9+
10+
# Create a new Kernel app
11+
app = kernel.App("python-persistent-browser")
12+
13+
class PageTitleInput(TypedDict):
14+
url: str
15+
16+
class PageTitleOutput(TypedDict):
17+
title: str
18+
elapsed_ms: float
19+
20+
@app.action("get-page-title")
21+
async def get_page_title(ctx: kernel.KernelContext, input_data: PageTitleInput) -> PageTitleOutput:
22+
"""
23+
A function that extracts the title of a webpage
24+
25+
Args:
26+
ctx: Kernel context containing invocation information
27+
input_data: An object with a URL property
28+
29+
Returns:
30+
A dictionary containing the page title
31+
"""
32+
url = input_data.get("url")
33+
if not url or not isinstance(url, str):
34+
raise ValueError("URL is required and must be a string")
35+
36+
# Add https:// if no protocol is present
37+
if not url.startswith(('http://', 'https://')):
38+
url = f"https://{url}"
39+
40+
# Validate the URL
41+
try:
42+
urlparse(url)
43+
except Exception:
44+
raise ValueError(f"Invalid URL: {url}")
45+
46+
# Create a browser instance using the context's invocation_id and a persistent id
47+
kernel_browser = client.browsers.create(
48+
invocation_id=ctx.invocation_id,
49+
persistence={"id": "my-awesome-persistent-browser-2"}
50+
)
51+
print("Kernel browser live view url: ", kernel_browser.browser_live_view_url)
52+
53+
async with async_playwright() as playwright:
54+
browser = await playwright.chromium.connect_over_cdp(kernel_browser.cdp_ws_url)
55+
try:
56+
now = datetime.now()
57+
context = len(browser.contexts) > 0 and browser.contexts[0] or await browser.new_context()
58+
page = len(context.pages) > 0 and context.pages[0] or await context.new_page()
59+
current_url = page.url
60+
print("Current url: ", current_url)
61+
if current_url != url:
62+
print("Not at url, navigating to it")
63+
await page.goto(url)
64+
else:
65+
print("Already at url, skipping navigation")
66+
# for some reason going straight to page.title() leads to an error: Page.title: Execution context was destroyed, most likely because of a navigation
67+
# calling bring_to_front() seems to fix it :shrug:
68+
await page.bring_to_front()
69+
title = await page.title()
70+
elapsedMilliseconds = (datetime.now() - now).total_seconds() * 1000
71+
return {"title": title, "elapsed_ms": elapsedMilliseconds}
72+
finally:
73+
await browser.close()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[project]
2+
name = "persistent-browser"
3+
version = "0.1.0"
4+
description = "Basic Kernel application that demonstrates the use of persistent browsers"
5+
readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = ["kernel==0.3.0", "playwright>=1.52.0"]
8+
9+
[dependency-groups]
10+
dev = ["mypy>=1.15.0"]

templates/python/persistent-browser/uv.lock

Lines changed: 354 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/python/sample-app/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
description = "Kernel application template - Python"
55
readme = "README.md"
66
requires-python = ">=3.11"
7-
dependencies = ["kernel==0.2.0", "playwright>=1.52.0"]
7+
dependencies = ["kernel==0.3.0", "playwright>=1.52.0"]
88

99
[dependency-groups]
1010
dev = ["mypy>=1.15.0"]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Dependencies
2+
node_modules/
3+
package-lock.json
4+
5+
# TypeScript
6+
*.tsbuildinfo
7+
dist/
8+
build/
9+
10+
# Environment
11+
.env
12+
.env.local
13+
.env.*.local
14+
15+
# IDE
16+
.vscode/
17+
.idea/
18+
*.swp
19+
*.swo
20+
21+
# OS
22+
.DS_Store
23+
Thumbs.db
24+
25+
# Logs
26+
logs/
27+
*.log
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
32+
# Testing
33+
coverage/
34+
.nyc_output/
35+
36+
# Misc
37+
.cache/
38+
.temp/
39+
.tmp/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Kernel Typscript Persistent Browser Example
2+
3+
This is a simple Kernel application that extracts the title from a webpage, but uses a persistent browser to avoid navigations on subsequent invocations.
4+
5+
See the [docs](https://docs.onkernel.com/quickstart) for information.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Kernel, type KernelContext } from "@onkernel/sdk";
2+
import { chromium, type BrowserContext, type Page } from "playwright";
3+
4+
const kernel = new Kernel();
5+
6+
const app = kernel.app("ts-persistent-browser");
7+
8+
interface PageTitleInput {
9+
url: string;
10+
}
11+
12+
interface PageTitleOutput {
13+
title: string;
14+
elapsed_ms: number;
15+
}
16+
17+
app.action<PageTitleInput, PageTitleOutput>(
18+
"get-page-title",
19+
async (
20+
ctx: KernelContext,
21+
payload?: PageTitleInput
22+
): Promise<PageTitleOutput> => {
23+
// A function that extracts the title of a webpage
24+
25+
// Args:
26+
// ctx: Kernel context containing invocation information
27+
// payload: An object with a URL property
28+
29+
// Returns:
30+
// A dictionary containing the page title
31+
if (!payload?.url) {
32+
throw new Error("URL is required");
33+
}
34+
35+
if (
36+
!payload.url.startsWith("http://") &&
37+
!payload.url.startsWith("https://")
38+
) {
39+
payload.url = `https://${payload.url}`;
40+
}
41+
42+
// Validate the URL
43+
try {
44+
new URL(payload.url);
45+
} catch {
46+
throw new Error(`Invalid URL: ${payload.url}`);
47+
}
48+
49+
const kernelBrowser = await kernel.browsers.create({
50+
invocation_id: ctx.invocation_id,
51+
persistence: {
52+
id: "ts-persistent-browser",
53+
},
54+
});
55+
56+
console.log(
57+
"Kernel browser live view url: ",
58+
kernelBrowser.browser_live_view_url
59+
);
60+
61+
const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
62+
try {
63+
const now = Date.now();
64+
const contexts = await browser.contexts();
65+
let context: BrowserContext = contexts[0] ?? (await browser.newContext());
66+
let page: Page = context.pages()[0] ?? (await context.newPage());
67+
if (page.url() !== payload.url) {
68+
console.log("navigating to", payload.url);
69+
await page.goto(payload.url);
70+
} else {
71+
console.log("page already loaded, skipping navigation");
72+
await page.bringToFront();
73+
}
74+
const title = await page.title();
75+
return { title, elapsed_ms: Date.now() - now };
76+
} finally {
77+
await browser.close();
78+
}
79+
}
80+
);

0 commit comments

Comments
 (0)