Skip to content

Commit fc7e7ff

Browse files
authored
Merge pull request #93 from SentienceAPI/async_api
Created async api; re-export in async_api
2 parents 2a313b7 + 770958f commit fc7e7ff

25 files changed

+3675
-1170
lines changed

examples/basic_agent_async.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Example: Basic agent usage (Async version)
3+
Demonstrates SentienceAgentAsync for natural language automation
4+
"""
5+
6+
import asyncio
7+
import os
8+
9+
from sentience.async_api import AsyncSentienceBrowser, SentienceAgentAsync
10+
from sentience.llm_provider import LLMProvider, LLMResponse
11+
12+
13+
# Simple mock LLM provider for demonstration
14+
# In production, use OpenAIProvider, AnthropicProvider, etc.
15+
class MockLLMProvider(LLMProvider):
16+
"""Mock LLM provider for testing"""
17+
18+
def generate(self, system_prompt: str, user_prompt: str, **kwargs) -> LLMResponse:
19+
# Simple mock that returns CLICK action
20+
return LLMResponse(
21+
content="CLICK(1)",
22+
model_name="mock-model",
23+
prompt_tokens=100,
24+
completion_tokens=10,
25+
total_tokens=110,
26+
)
27+
28+
def supports_json_mode(self) -> bool:
29+
return True
30+
31+
@property
32+
def model_name(self) -> str:
33+
return "mock-model"
34+
35+
36+
async def main():
37+
# Get API key from environment variable (optional - uses free tier if not set)
38+
api_key = os.environ.get("SENTIENCE_API_KEY")
39+
40+
async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser:
41+
# Navigate to a page
42+
await browser.goto("https://example.com", wait_until="domcontentloaded")
43+
44+
# Create LLM provider
45+
# In production, use: llm = OpenAIProvider(api_key="your-key", model="gpt-4o")
46+
llm = MockLLMProvider()
47+
48+
# Create agent
49+
agent = SentienceAgentAsync(browser, llm, verbose=True)
50+
51+
print("=== Basic Agent Demo ===\n")
52+
53+
# Example 1: Simple action
54+
print("1. Executing simple action...")
55+
try:
56+
result = await agent.act("Click the first link")
57+
print(f" Result: success={result.success}, action={result.action}")
58+
if result.element_id:
59+
print(f" Clicked element ID: {result.element_id}")
60+
except Exception as e:
61+
print(f" Error: {e}")
62+
63+
print()
64+
65+
# Example 2: Check history
66+
print("2. Agent execution history:")
67+
history = agent.get_history()
68+
print(f" Total actions: {len(history)}")
69+
for i, entry in enumerate(history, 1):
70+
print(f" {i}. {entry.goal} -> {entry.action} (success: {entry.success})")
71+
72+
print()
73+
74+
# Example 3: Token statistics
75+
print("3. Token usage statistics:")
76+
stats = agent.get_token_stats()
77+
print(f" Total tokens: {stats.total_tokens}")
78+
print(f" Prompt tokens: {stats.total_prompt_tokens}")
79+
print(f" Completion tokens: {stats.total_completion_tokens}")
80+
81+
print()
82+
83+
# Example 4: Clear history
84+
print("4. Clearing history...")
85+
agent.clear_history()
86+
print(f" History length after clear: {len(agent.get_history())}")
87+
88+
print("\n✅ Basic agent demo complete!")
89+
print("\nNote: This example uses a mock LLM provider.")
90+
print("In production, use a real LLM provider like OpenAIProvider or AnthropicProvider.")
91+
92+
93+
if __name__ == "__main__":
94+
asyncio.run(main())

examples/hello_async.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Example: Verify extension bridge is loaded (Async version)
3+
"""
4+
5+
import asyncio
6+
import os
7+
8+
from sentience.async_api import AsyncSentienceBrowser
9+
10+
11+
async def main():
12+
# Get API key from environment variable (optional - uses free tier if not set)
13+
api_key = os.environ.get("SENTIENCE_API_KEY")
14+
15+
try:
16+
async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser:
17+
# Navigate to a page to ensure extension is active
18+
await browser.goto("https://example.com", wait_until="domcontentloaded")
19+
20+
# Check if extension API is available
21+
bridge_ok = await browser.page.evaluate(
22+
"""
23+
() => {
24+
return typeof window.sentience !== 'undefined' &&
25+
typeof window.sentience.snapshot === 'function';
26+
}
27+
"""
28+
)
29+
print(f"bridge_ok={bridge_ok}")
30+
31+
if bridge_ok:
32+
print("✅ Extension loaded successfully!")
33+
# Try a quick snapshot to verify it works
34+
try:
35+
result = await browser.page.evaluate("window.sentience.snapshot({ limit: 1 })")
36+
if result.get("status") == "success":
37+
print(f"✅ Snapshot test: Found {len(result.get('elements', []))} elements")
38+
else:
39+
print(f"⚠️ Snapshot returned: {result.get('status')}")
40+
except Exception as e:
41+
print(f"⚠️ Snapshot test failed: {e}")
42+
else:
43+
print("❌ Extension not loaded")
44+
# Debug info
45+
debug_info = await browser.page.evaluate(
46+
"""
47+
() => {
48+
return {
49+
sentience_defined: typeof window.sentience !== 'undefined',
50+
registry_defined: typeof window.sentience_registry !== 'undefined',
51+
snapshot_defined: typeof window.sentience?.snapshot !== 'undefined'
52+
};
53+
}
54+
"""
55+
)
56+
print(f"Debug info: {debug_info}")
57+
except Exception as e:
58+
print(f"❌ Error: {e}")
59+
import traceback
60+
61+
traceback.print_exc()
62+
63+
64+
if __name__ == "__main__":
65+
asyncio.run(main())

examples/query_demo_async.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Example: Query engine demonstration (Async version)
3+
"""
4+
5+
import asyncio
6+
import os
7+
8+
from sentience.async_api import AsyncSentienceBrowser, find, query, snapshot_async
9+
10+
11+
async def main():
12+
# Get API key from environment variable (optional - uses free tier if not set)
13+
api_key = os.environ.get("SENTIENCE_API_KEY")
14+
15+
async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser:
16+
# Navigate to a page with links
17+
await browser.goto("https://example.com", wait_until="domcontentloaded")
18+
19+
snap = await snapshot_async(browser)
20+
21+
# Query examples
22+
print("=== Query Examples ===\n")
23+
24+
# Find all buttons
25+
buttons = query(snap, "role=button")
26+
print(f"Found {len(buttons)} buttons")
27+
28+
# Find all links
29+
links = query(snap, "role=link")
30+
print(f"Found {len(links)} links")
31+
32+
# Find clickable elements
33+
clickables = query(snap, "clickable=true")
34+
print(f"Found {len(clickables)} clickable elements")
35+
36+
# Find element with text containing "More"
37+
more_link = find(snap, "text~'More'")
38+
if more_link:
39+
print(f"\nFound 'More' link: {more_link.text} (id: {more_link.id})")
40+
else:
41+
print("\nNo 'More' link found")
42+
43+
# Complex query: clickable links
44+
clickable_links = query(snap, "role=link clickable=true")
45+
print(f"\nFound {len(clickable_links)} clickable links")
46+
47+
48+
if __name__ == "__main__":
49+
asyncio.run(main())

examples/read_markdown_async.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Example: Reading page content and converting to markdown (Async version)
3+
4+
This example shows how to use the read_async() function to get page content
5+
and convert it to high-quality markdown using markdownify.
6+
"""
7+
8+
import asyncio
9+
import os
10+
11+
from markdownify import markdownify
12+
13+
from sentience.async_api import AsyncSentienceBrowser, read_async
14+
15+
16+
async def main():
17+
# Get API key from environment variable (optional - uses free tier if not set)
18+
api_key = os.environ.get("SENTIENCE_API_KEY")
19+
20+
# Initialize browser
21+
async with AsyncSentienceBrowser(api_key=api_key, headless=True) as browser:
22+
# Navigate to a page
23+
await browser.goto("https://example.com", wait_until="domcontentloaded")
24+
25+
# Method 1: Get raw HTML (default) and convert with markdownify
26+
print("=== Method 1: Raw HTML + markdownify (Recommended) ===")
27+
result = await read_async(browser) # output_format="raw" is default
28+
html_content = result["content"]
29+
30+
# Convert to markdown using markdownify (better quality)
31+
markdown = markdownify(
32+
html_content,
33+
heading_style="ATX", # Use # for headings
34+
bullets="-", # Use - for lists
35+
strip=["script", "style", "nav", "footer", "header"], # Strip unwanted tags
36+
)
37+
print(f"Markdown length: {len(markdown)} characters")
38+
print(markdown[:500]) # Print first 500 chars
39+
print("\n")
40+
41+
# Method 2: Get high-quality markdown directly (uses markdownify internally)
42+
print("=== Method 2: Direct markdown (High-quality via markdownify) ===")
43+
result = await read_async(browser, output_format="markdown")
44+
high_quality_markdown = result["content"]
45+
print(f"Markdown length: {len(high_quality_markdown)} characters")
46+
print(high_quality_markdown[:500]) # Print first 500 chars
47+
print("\n")
48+
49+
# Method 3: Get plain text
50+
print("=== Method 3: Plain text ===")
51+
result = await read_async(browser, output_format="text")
52+
text_content = result["content"]
53+
print(f"Text length: {len(text_content)} characters")
54+
print(text_content[:500]) # Print first 500 chars
55+
56+
57+
if __name__ == "__main__":
58+
asyncio.run(main())
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Example: Semantic wait_for using query DSL (Async version)
3+
Demonstrates waiting for elements using semantic selectors
4+
"""
5+
6+
import asyncio
7+
import os
8+
9+
from sentience.async_api import AsyncSentienceBrowser, click_async, wait_for_async
10+
11+
12+
async def main():
13+
# Get API key from environment variable (optional - uses free tier if not set)
14+
api_key = os.environ.get("SENTIENCE_API_KEY")
15+
16+
async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser:
17+
# Navigate to example.com
18+
await browser.goto("https://example.com", wait_until="domcontentloaded")
19+
20+
print("=== Semantic wait_for_async Demo ===\n")
21+
22+
# Example 1: Wait for element by role
23+
print("1. Waiting for link element (role=link)")
24+
wait_result = await wait_for_async(browser, "role=link", timeout=5.0)
25+
if wait_result.found:
26+
print(f" ✅ Found after {wait_result.duration_ms}ms")
27+
print(f" Element: '{wait_result.element.text}' (id: {wait_result.element.id})")
28+
else:
29+
print(f" ❌ Not found (timeout: {wait_result.timeout})")
30+
print()
31+
32+
# Example 2: Wait for element by role and text
33+
print("2. Waiting for link with specific text")
34+
wait_result = await wait_for_async(browser, "role=link text~'Example'", timeout=5.0)
35+
if wait_result.found:
36+
print(f" ✅ Found after {wait_result.duration_ms}ms")
37+
print(f" Element text: '{wait_result.element.text}'")
38+
else:
39+
print(" ❌ Not found")
40+
print()
41+
42+
# Example 3: Wait for clickable element
43+
print("3. Waiting for clickable element")
44+
wait_result = await wait_for_async(browser, "clickable=true", timeout=5.0)
45+
if wait_result.found:
46+
print(f" ✅ Found clickable element after {wait_result.duration_ms}ms")
47+
print(f" Role: {wait_result.element.role}")
48+
print(f" Text: '{wait_result.element.text}'")
49+
print(f" Is clickable: {wait_result.element.visual_cues.is_clickable}")
50+
else:
51+
print(" ❌ Not found")
52+
print()
53+
54+
# Example 4: Wait for element with importance threshold
55+
print("4. Waiting for important element (importance > 100)")
56+
wait_result = await wait_for_async(browser, "importance>100", timeout=5.0)
57+
if wait_result.found:
58+
print(f" ✅ Found important element after {wait_result.duration_ms}ms")
59+
print(f" Importance: {wait_result.element.importance}")
60+
print(f" Role: {wait_result.element.role}")
61+
else:
62+
print(" ❌ Not found")
63+
print()
64+
65+
# Example 5: Wait and then click
66+
print("5. Wait for element, then click it")
67+
wait_result = await wait_for_async(browser, "role=link", timeout=5.0)
68+
if wait_result.found:
69+
print(" ✅ Found element, clicking...")
70+
click_result = await click_async(browser, wait_result.element.id)
71+
print(
72+
f" Click result: success={click_result.success}, outcome={click_result.outcome}"
73+
)
74+
if click_result.url_changed:
75+
print(f" ✅ Navigation occurred: {browser.page.url}")
76+
else:
77+
print(" ❌ Element not found, cannot click")
78+
print()
79+
80+
# Example 6: Using local extension (fast polling)
81+
print("6. Using local extension with auto-optimized interval")
82+
print(" When use_api=False, interval auto-adjusts to 0.25s (fast)")
83+
wait_result = await wait_for_async(browser, "role=link", timeout=5.0, use_api=False)
84+
if wait_result.found:
85+
print(f" ✅ Found after {wait_result.duration_ms}ms")
86+
print(" (Used local extension, polled every 0.25 seconds)")
87+
print()
88+
89+
# Example 7: Using remote API (slower polling)
90+
print("7. Using remote API with auto-optimized interval")
91+
print(" When use_api=True, interval auto-adjusts to 1.5s (network-friendly)")
92+
if api_key:
93+
wait_result = await wait_for_async(browser, "role=link", timeout=5.0, use_api=True)
94+
if wait_result.found:
95+
print(f" ✅ Found after {wait_result.duration_ms}ms")
96+
print(" (Used remote API, polled every 1.5 seconds)")
97+
else:
98+
print(" ⚠️ Skipped (no API key set)")
99+
print()
100+
101+
# Example 8: Custom interval override
102+
print("8. Custom interval override (manual control)")
103+
print(" You can still specify custom interval if needed")
104+
wait_result = await wait_for_async(
105+
browser, "role=link", timeout=5.0, interval=0.5, use_api=False
106+
)
107+
if wait_result.found:
108+
print(f" ✅ Found after {wait_result.duration_ms}ms")
109+
print(" (Custom interval: 0.5 seconds)")
110+
print()
111+
112+
print("✅ Semantic wait_for_async demo complete!")
113+
print("\nNote: wait_for_async uses the semantic query DSL to find elements.")
114+
print("This is more robust than CSS selectors because it understands")
115+
print("the semantic meaning of elements (role, text, clickability, etc.).")
116+
117+
118+
if __name__ == "__main__":
119+
asyncio.run(main())

0 commit comments

Comments
 (0)