diff --git a/docs/content/docs/agent-sdk/agent-loops.mdx b/docs/content/docs/agent-sdk/agent-loops.mdx
index 2885a5c5..49d7e897 100644
--- a/docs/content/docs/agent-sdk/agent-loops.mdx
+++ b/docs/content/docs/agent-sdk/agent-loops.mdx
@@ -4,11 +4,7 @@ description: Supported computer-using agent loops and models
---
- A corresponding{' '}
-
- Jupyter Notebook
- {' '}
- is available for this documentation.
+ A corresponding Jupyter Notebook is available for this documentation.
An agent can be thought of as a loop - it generates actions, executes them, and repeats until done:
diff --git a/docs/content/docs/agent-sdk/customizing-computeragent.mdx b/docs/content/docs/agent-sdk/customizing-computeragent.mdx
index e7d3c030..82eace76 100644
--- a/docs/content/docs/agent-sdk/customizing-computeragent.mdx
+++ b/docs/content/docs/agent-sdk/customizing-computeragent.mdx
@@ -1,16 +1,9 @@
---
-title: Customizing Your ComputerAgent
+title: Customize ComputerAgent
---
- A corresponding{' '}
-
- Jupyter Notebook
- {' '}
- is available for this documentation.
+ A corresponding Jupyter Notebook is available for this documentation.
The `ComputerAgent` interface provides an easy proxy to any computer-using model configuration, and it is a powerful framework for extending and building your own agentic systems.
diff --git a/docs/content/docs/agent-sdk/integrations/hud.mdx b/docs/content/docs/agent-sdk/integrations/hud.mdx
index 7bfcbdea..9575ebf6 100644
--- a/docs/content/docs/agent-sdk/integrations/hud.mdx
+++ b/docs/content/docs/agent-sdk/integrations/hud.mdx
@@ -4,11 +4,7 @@ description: Use ComputerAgent with HUD for benchmarking and evaluation
---
- A corresponding{' '}
-
- Jupyter Notebook
- {' '}
- is available for this documentation.
+ A corresponding Jupyter Notebook is available for this documentation.
The HUD integration allows an agent to be benchmarked using the [HUD framework](https://www.hud.so/). Through the HUD integration, the agent controls a computer inside HUD, where tests are run to evaluate the success of each task.
diff --git a/docs/content/docs/agent-sdk/meta.json b/docs/content/docs/agent-sdk/meta.json
index b86632e7..0a733f28 100644
--- a/docs/content/docs/agent-sdk/meta.json
+++ b/docs/content/docs/agent-sdk/meta.json
@@ -10,12 +10,10 @@
"customizing-computeragent",
"callbacks",
"custom-tools",
- "custom-computer-handlers",
"prompt-caching",
"usage-tracking",
"telemetry",
"benchmarks",
- "migration-guide",
"integrations"
]
}
diff --git a/docs/content/docs/computer-sdk/computer-ui.mdx b/docs/content/docs/computer-sdk/computer-ui.mdx
index c731e4c4..9739398b 100644
--- a/docs/content/docs/computer-sdk/computer-ui.mdx
+++ b/docs/content/docs/computer-sdk/computer-ui.mdx
@@ -1,7 +1,11 @@
---
-title: Computer UI
+title: Computer UI (Deprecated)
---
+
+ The Computer UI is deprecated and will be replaced with a revamped playground experience soon. We recommend using VNC or Screen Sharing for precise control of the computer instead.
+
+
The computer module includes a Gradio UI for creating and sharing demonstration data. We make it easy for people to build community datasets for better computer use models with an upload to Huggingface feature.
```bash
diff --git a/docs/content/docs/agent-sdk/custom-computer-handlers.mdx b/docs/content/docs/computer-sdk/custom-computer-handlers.mdx
similarity index 100%
rename from docs/content/docs/agent-sdk/custom-computer-handlers.mdx
rename to docs/content/docs/computer-sdk/custom-computer-handlers.mdx
diff --git a/docs/content/docs/computer-sdk/meta.json b/docs/content/docs/computer-sdk/meta.json
index 547dde17..f2c124e7 100644
--- a/docs/content/docs/computer-sdk/meta.json
+++ b/docs/content/docs/computer-sdk/meta.json
@@ -1,5 +1,5 @@
{
"title": "Computer SDK",
"description": "Build computer-using agents with the Computer SDK",
- "pages": ["computers", "commands", "computer-ui", "tracing-api", "sandboxed-python"]
+ "pages": ["computers", "commands", "tracing-api", "sandboxed-python", "custom-computer-handlers", "computer-ui"]
}
diff --git a/docs/content/docs/computer-sdk/sandboxed-python.mdx b/docs/content/docs/computer-sdk/sandboxed-python.mdx
index bb1c1e9c..e66ad34c 100644
--- a/docs/content/docs/computer-sdk/sandboxed-python.mdx
+++ b/docs/content/docs/computer-sdk/sandboxed-python.mdx
@@ -4,14 +4,7 @@ slug: sandboxed-python
---
- A corresponding{' '}
-
- Python example
- {' '}
- is available for this documentation.
+ A corresponding Python example is available for this documentation.
You can run Python functions securely inside a sandboxed virtual environment on a remote Cua Computer. This is useful for executing untrusted user code, isolating dependencies, or providing a safe environment for automation tasks.
diff --git a/docs/content/docs/example-usecases/form-filling.mdx b/docs/content/docs/example-usecases/form-filling.mdx
index b6f60b05..7a15cd5f 100644
--- a/docs/content/docs/example-usecases/form-filling.mdx
+++ b/docs/content/docs/example-usecases/form-filling.mdx
@@ -3,7 +3,7 @@ title: Form Filling
description: Enhance and Automate Interactions Between Form Filling and Local File Systems
---
-import { EditableCodeBlock, EditableValue, S } from '@/components/editable-code-block';
+import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Overview
@@ -12,9 +12,17 @@ Cua can be used to automate interactions between form filling and local file sys
This preset usecase uses [Cua Computer](/computer-sdk/computers) to interact with a web page and local file systems along with [Agent Loops](/agent-sdk/agent-loops) to run the agent in a loop with message history.
-## Quickstart
+---
-Create a `requirements.txt` file with the following dependencies:
+
+
+
+
+### Set Up Your Environment
+
+First, install the required dependencies:
+
+Create a `requirements.txt` file:
```text
cua-agent
@@ -22,33 +30,32 @@ cua-computer
python-dotenv>=1.0.0
```
-And install:
+Install the dependencies:
```bash
pip install -r requirements.txt
```
-Create a `.env` file with the following environment variables:
+Create a `.env` file with your API keys:
```text
-ANTHROPIC_API_KEY=your-api-key
+ANTHROPIC_API_KEY=your-anthropic-api-key
CUA_API_KEY=sk_cua-api01...
```
-Select the environment you want to run the code in (_click on the underlined values in the code to edit them directly!_):
+
-
-
+
-
-{`import asyncio
+### Create Your Form Filling Script
+
+Create a Python file (e.g., `form_filling.py`) and select your environment:
+
+
+
+
+```python
+import asyncio
import logging
import os
import signal
@@ -59,21 +66,21 @@ from computer import Computer, VMProviderType
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
+logger = logging.getLogger(__name__)
def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
+ print("\n\nExecution interrupted by user. Exiting gracefully...")
+ exit(0)
async def fill_application():
-try:
-async with Computer(
-os_type="linux",
-provider_type=VMProviderType.CLOUD,
-name="`}{`",
-api_key="`}{`",
-verbosity=logging.INFO,
-) as computer:
+ try:
+ async with Computer(
+ os_type="linux",
+ provider_type=VMProviderType.CLOUD,
+ name="your-sandbox-name", # Replace with your sandbox name
+ api_key=os.environ["CUA_API_KEY"],
+ verbosity=logging.INFO,
+ ) as computer:
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929",
@@ -93,7 +100,7 @@ verbosity=logging.INFO,
history = []
for i, task in enumerate(tasks, 1):
- print(f"\\n[Task {i}/{len(tasks)}] {task}")
+ print(f"\n[Task {i}/{len(tasks)}] {task}")
# Add user message to history
history.append({"role": "user", "content": task})
@@ -116,7 +123,7 @@ verbosity=logging.INFO,
print(f"ā
Task {i}/{len(tasks)} completed")
- print("\\nš All tasks completed successfully!")
+ print("\nš All tasks completed successfully!")
except Exception as e:
logger.error(f"Error in fill_application: {e}")
@@ -124,18 +131,18 @@ verbosity=logging.INFO,
raise
def main():
-try:
-load_dotenv()
+ try:
+ load_dotenv()
if "ANTHROPIC_API_KEY" not in os.environ:
raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
+ "Please set the ANTHROPIC_API_KEY environment variable.\n"
"You can add it to a .env file in the project root."
)
if "CUA_API_KEY" not in os.environ:
raise RuntimeError(
- "Please set the CUA_API_KEY environment variable.\\n"
+ "Please set the CUA_API_KEY environment variable.\n"
"You can add it to a .env file in the project root."
)
@@ -147,22 +154,15 @@ load_dotenv()
logger.error(f"Error running automation: {e}")
traceback.print_exc()
-if **name** == "**main**":
-main()`}
-
-
+if __name__ == "__main__":
+ main()
+```
-
+
-
-{`import asyncio
+```python
+import asyncio
import logging
import os
import signal
@@ -173,20 +173,20 @@ from computer import Computer, VMProviderType
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
+logger = logging.getLogger(__name__)
def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
+ print("\n\nExecution interrupted by user. Exiting gracefully...")
+ exit(0)
async def fill_application():
-try:
-async with Computer(
-os_type="macos",
-provider_type=VMProviderType.LUME,
-name="`}{`",
-verbosity=logging.INFO,
-) as computer:
+ try:
+ async with Computer(
+ os_type="linux",
+ provider_type=VMProviderType.DOCKER,
+ image="trycua/cua-xfce:latest", # or "trycua/cua-ubuntu:latest"
+ verbosity=logging.INFO,
+ ) as computer:
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929",
@@ -206,7 +206,7 @@ verbosity=logging.INFO,
history = []
for i, task in enumerate(tasks, 1):
- print(f"\\n[Task {i}/{len(tasks)}] {task}")
+ print(f"\n[Task {i}/{len(tasks)}] {task}")
# Add user message to history
history.append({"role": "user", "content": task})
@@ -229,7 +229,7 @@ verbosity=logging.INFO,
print(f"ā
Task {i}/{len(tasks)} completed")
- print("\\nš All tasks completed successfully!")
+ print("\nš All tasks completed successfully!")
except Exception as e:
logger.error(f"Error in fill_application: {e}")
@@ -237,12 +237,12 @@ verbosity=logging.INFO,
raise
def main():
-try:
-load_dotenv()
+ try:
+ load_dotenv()
if "ANTHROPIC_API_KEY" not in os.environ:
raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
+ "Please set the ANTHROPIC_API_KEY environment variable.\n"
"You can add it to a .env file in the project root."
)
@@ -254,20 +254,15 @@ load_dotenv()
logger.error(f"Error running automation: {e}")
traceback.print_exc()
-if **name** == "**main**":
-main()`}
-
-
+if __name__ == "__main__":
+ main()
+```
-
+
-
-{`import asyncio
+```python
+import asyncio
import logging
import os
import signal
@@ -278,19 +273,20 @@ from computer import Computer, VMProviderType
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
+logger = logging.getLogger(__name__)
def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
+ print("\n\nExecution interrupted by user. Exiting gracefully...")
+ exit(0)
async def fill_application():
-try:
-async with Computer(
-os_type="windows",
-provider_type=VMProviderType.WINDOWS_SANDBOX,
-verbosity=logging.INFO,
-) as computer:
+ try:
+ async with Computer(
+ os_type="macos",
+ provider_type=VMProviderType.LUME,
+ name="macos-sequoia-cua:latest",
+ verbosity=logging.INFO,
+ ) as computer:
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929",
@@ -310,7 +306,7 @@ verbosity=logging.INFO,
history = []
for i, task in enumerate(tasks, 1):
- print(f"\\n[Task {i}/{len(tasks)}] {task}")
+ print(f"\n[Task {i}/{len(tasks)}] {task}")
# Add user message to history
history.append({"role": "user", "content": task})
@@ -333,7 +329,7 @@ verbosity=logging.INFO,
print(f"ā
Task {i}/{len(tasks)} completed")
- print("\\nš All tasks completed successfully!")
+ print("\nš All tasks completed successfully!")
except Exception as e:
logger.error(f"Error in fill_application: {e}")
@@ -341,12 +337,12 @@ verbosity=logging.INFO,
raise
def main():
-try:
-load_dotenv()
+ try:
+ load_dotenv()
if "ANTHROPIC_API_KEY" not in os.environ:
raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
+ "Please set the ANTHROPIC_API_KEY environment variable.\n"
"You can add it to a .env file in the project root."
)
@@ -358,22 +354,15 @@ load_dotenv()
logger.error(f"Error running automation: {e}")
traceback.print_exc()
-if **name** == "**main**":
-main()`}
-
-
+if __name__ == "__main__":
+ main()
+```
-
+
-
-{`import asyncio
+```python
+import asyncio
import logging
import os
import signal
@@ -384,20 +373,19 @@ from computer import Computer, VMProviderType
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
+logger = logging.getLogger(__name__)
def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
+ print("\n\nExecution interrupted by user. Exiting gracefully...")
+ exit(0)
async def fill_application():
-try:
-async with Computer(
-os_type="linux",
-provider_type=VMProviderType.DOCKER,
-name="`}{`",
-verbosity=logging.INFO,
-) as computer:
+ try:
+ async with Computer(
+ os_type="windows",
+ provider_type=VMProviderType.WINDOWS_SANDBOX,
+ verbosity=logging.INFO,
+ ) as computer:
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929",
@@ -417,7 +405,7 @@ verbosity=logging.INFO,
history = []
for i, task in enumerate(tasks, 1):
- print(f"\\n[Task {i}/{len(tasks)}] {task}")
+ print(f"\n[Task {i}/{len(tasks)}] {task}")
# Add user message to history
history.append({"role": "user", "content": task})
@@ -440,7 +428,7 @@ verbosity=logging.INFO,
print(f"ā
Task {i}/{len(tasks)} completed")
- print("\\nš All tasks completed successfully!")
+ print("\nš All tasks completed successfully!")
except Exception as e:
logger.error(f"Error in fill_application: {e}")
@@ -448,12 +436,12 @@ verbosity=logging.INFO,
raise
def main():
-try:
-load_dotenv()
+ try:
+ load_dotenv()
if "ANTHROPIC_API_KEY" not in os.environ:
raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
+ "Please set the ANTHROPIC_API_KEY environment variable.\n"
"You can add it to a .env file in the project root."
)
@@ -465,16 +453,41 @@ load_dotenv()
logger.error(f"Error running automation: {e}")
traceback.print_exc()
-if **name** == "**main**":
-main()`}
-
-
+if __name__ == "__main__":
+ main()
+```
+
+
+
+
+### Run Your Script
+
+Execute your form filling automation:
+
+```bash
+python form_filling.py
+```
+
+The agent will:
+1. Download the PDF resume from Overleaf
+2. Extract information from the PDF
+3. Fill out the JotForm with the extracted information
+
+Monitor the output to see the agent's progress through each task.
+
+
+
+
+
+---
+
## Next Steps
- Learn more about [Cua computers](/computer-sdk/computers) and [computer commands](/computer-sdk/commands)
- Read about [Agent loops](/agent-sdk/agent-loops), [tools](/agent-sdk/custom-tools), and [supported model providers](/agent-sdk/supported-model-providers/)
- Experiment with different [Models and Providers](/agent-sdk/supported-model-providers/)
+- Join our [Discord community](https://discord.com/invite/mVnXXpdE85) for help
diff --git a/docs/content/docs/example-usecases/post-event-contact-export.mdx b/docs/content/docs/example-usecases/post-event-contact-export.mdx
index fcc6e3f7..16131702 100644
--- a/docs/content/docs/example-usecases/post-event-contact-export.mdx
+++ b/docs/content/docs/example-usecases/post-event-contact-export.mdx
@@ -3,7 +3,7 @@ title: Post-Event Contact Export
description: Run overnight contact extraction from LinkedIn, X, or other social platforms after networking events
---
-import { EditableCodeBlock, EditableValue, S } from '@/components/editable-code-block';
+import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Overview
@@ -26,7 +26,7 @@ This example focuses on LinkedIn but works across platforms. It uses [Cua Comput
Traditional web scraping triggers anti-bot measures immediately. Cua's approach works across all platforms.
-## What You Get
+### What You Get
The script generates two files with your extracted connections:
@@ -36,10 +36,6 @@ The script generates two files with your extracted connections:
first,last,role,company,met_at,linkedin
John,Smith,Software Engineer,Acme Corp,Google Devfest Toronto,https://www.linkedin.com/in/johnsmith
Sarah,Johnson,Product Manager,Tech Inc,Google Devfest Toronto,https://www.linkedin.com/in/sarahjohnson
-Michael,Chen,Data Scientist,StartupXYZ,Google Devfest Toronto,https://www.linkedin.com/in/michaelchen
-Emily,Rodriguez,UX Designer,Design Co,Google Devfest Toronto,https://www.linkedin.com/in/emilyrodriguez
-David,Kim,Engineering Lead,BigTech,Google Devfest Toronto,https://www.linkedin.com/in/davidkim
-...
```
**Messaging Links** (`linkedin_messaging_links_20250116_143022.txt`):
@@ -50,15 +46,19 @@ LinkedIn Messaging Compose Links
1. https://www.linkedin.com/messaging/compose/?recipient=johnsmith
2. https://www.linkedin.com/messaging/compose/?recipient=sarahjohnson
-3. https://www.linkedin.com/messaging/compose/?recipient=michaelchen
-4. https://www.linkedin.com/messaging/compose/?recipient=emilyrodriguez
-5. https://www.linkedin.com/messaging/compose/?recipient=davidkim
-...
```
-## Quickstart
+---
-Create a `requirements.txt` file with the following dependencies:
+
+
+
+
+### Set Up Your Environment
+
+First, install the required dependencies:
+
+Create a `requirements.txt` file:
```text
cua-agent
@@ -66,20 +66,26 @@ cua-computer
python-dotenv>=1.0.0
```
-And install:
+Install the dependencies:
```bash
pip install -r requirements.txt
```
-Create a `.env` file with the following environment variables:
+Create a `.env` file with your API keys:
```text
-ANTHROPIC_API_KEY=your-api-key
+ANTHROPIC_API_KEY=your-anthropic-api-key
CUA_API_KEY=sk_cua-api01...
CUA_CONTAINER_NAME=m-linux-...
```
+
+
+
+
+### Log Into LinkedIn Manually
+
**Important**: Before running the script, manually log into LinkedIn through your VM:
1. Access your VM through the Cua dashboard
@@ -88,31 +94,31 @@ CUA_CONTAINER_NAME=m-linux-...
4. Close the browser but leave the VM running
5. Your session is now saved and ready for automation!
-**Configuration**: Customize the script by editing these variables:
+This one-time manual login bypasses all bot detection.
+
+
+
+
+
+### Configure and Create Your Script
+
+Create a Python file (e.g., `contact_export.py`). You can customize:
```python
# Where you met these connections (automatically added to CSV)
MET_AT_REASON = "Google Devfest Toronto"
-# Number of contacts to extract (line 134)
+# Number of contacts to extract (in the main loop)
for contact_num in range(1, 21): # Change 21 to extract more/fewer contacts
```
-Select the environment you want to run the code in (_click on the underlined values in the code to edit them directly!_):
+Select your environment:
-
-
+
+
-
-{`import asyncio
+```python
+import asyncio
import csv
import logging
import os
@@ -125,28 +131,22 @@ from computer import Computer, VMProviderType
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
+logger = logging.getLogger(__name__)
# Configuration: Define where you met these connections
-
-MET_AT_REASON = "`}{`"
+MET_AT_REASON = "Google Devfest Toronto"
def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
+ print("\n\nExecution interrupted by user. Exiting gracefully...")
+ exit(0)
def extract_public_id_from_linkedin_url(linkedin_url):
-"""
-Extract public ID from LinkedIn profile URL.
-Example: https://www.linkedin.com/in/taylor-r-devries/?lipi=... -> taylor-r-devries
-"""
-if not linkedin_url:
-return None
+ """Extract public ID from LinkedIn profile URL."""
+ if not linkedin_url:
+ return None
- # Remove query parameters and trailing slashes
url = linkedin_url.split('?')[0].rstrip('/')
- # Extract the part after /in/
if '/in/' in url:
public_id = url.split('/in/')[-1]
return public_id
@@ -154,88 +154,70 @@ return None
return None
def extract_contact_from_response(result_output):
-"""
-Extract contact information from agent's response.
-Expects the agent to return data in format:
-FIRST: value
-LAST: value
-ROLE: value
-COMPANY: value
-LINKEDIN: value
-
- Note: met_at is auto-filled from MET_AT_REASON constant.
+ """
+ Extract contact information from agent's response.
+ Expects format:
+ FIRST: value
+ LAST: value
+ ROLE: value
+ COMPANY: value
+ LINKEDIN: value
"""
contact = {
'first': '',
'last': '',
'role': '',
'company': '',
- 'met_at': MET_AT_REASON, # Auto-fill from constant
+ 'met_at': MET_AT_REASON,
'linkedin': ''
}
- # Collect all text from messages for debugging
- all_text = []
-
for item in result_output:
if item.get("type") == "message":
content = item.get("content", [])
for content_part in content:
text = content_part.get("text", "")
if text:
- all_text.append(text)
- # Parse structured output - look for the exact format
- for line in text.split('\\n'):
+ for line in text.split('\n'):
line = line.strip()
- # Use case-insensitive matching and handle extra whitespace
line_upper = line.upper()
if line_upper.startswith("FIRST:"):
- value = line[6:].strip() # Skip "FIRST:" prefix
+ value = line[6:].strip()
if value and value.upper() != "N/A":
contact['first'] = value
elif line_upper.startswith("LAST:"):
- value = line[5:].strip() # Skip "LAST:" prefix
+ value = line[5:].strip()
if value and value.upper() != "N/A":
contact['last'] = value
elif line_upper.startswith("ROLE:"):
- value = line[5:].strip() # Skip "ROLE:" prefix
+ value = line[5:].strip()
if value and value.upper() != "N/A":
contact['role'] = value
elif line_upper.startswith("COMPANY:"):
- value = line[8:].strip() # Skip "COMPANY:" prefix
+ value = line[8:].strip()
if value and value.upper() != "N/A":
contact['company'] = value
elif line_upper.startswith("LINKEDIN:"):
- value = line[9:].strip() # Skip "LINKEDIN:" prefix
+ value = line[9:].strip()
if value and value.upper() != "N/A":
contact['linkedin'] = value
- # Debug logging
- if not (contact['first'] or contact['last'] or contact['linkedin']):
- logger.debug(f"Failed to extract. Full text content ({len(all_text)} messages):")
- for i, text in enumerate(all_text[-3:]): # Show last 3 messages
- logger.debug(f" Message {i}: {text[:200]}")
-
return contact
async def scrape_linkedin_connections():
-"""
-Scrape the first 20 connections from LinkedIn and export to CSV.
-The agent extracts data, and Python handles CSV writing programmatically.
-"""
+ """Scrape LinkedIn connections and export to CSV."""
- # Generate output filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_filename = f"linkedin_connections_{timestamp}.csv"
csv_path = os.path.join(os.getcwd(), csv_filename)
- # Initialize CSV file with headers
+ # Initialize CSV file
with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
writer.writeheader()
- print(f"\\nš Starting LinkedIn connections scraper")
+ print(f"\nš Starting LinkedIn connections scraper")
print(f"š Output file: {csv_path}")
print(f"š Met at: {MET_AT_REASON}")
print("=" * 80)
@@ -244,8 +226,8 @@ The agent extracts data, and Python handles CSV writing programmatically.
async with Computer(
os_type="linux",
provider_type=VMProviderType.CLOUD,
- name="`}{`",
- api_key="`}{`",
+ name=os.environ["CUA_CONTAINER_NAME"], # Your sandbox name
+ api_key=os.environ["CUA_API_KEY"],
verbosity=logging.INFO,
) as computer:
@@ -263,1257 +245,224 @@ The agent extracts data, and Python handles CSV writing programmatically.
# Task 1: Navigate to LinkedIn connections page
navigation_task = (
- "STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\\n"
- "1. Open a web browser (Chrome or Firefox)\\n"
- "2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\\n"
- "3. Wait for the page to fully load (look for the connection list to appear)\\n"
- "4. If prompted to log in, handle the authentication\\n"
- "5. Confirm you can see the list of connections displayed on the page\\n"
- "6. Ready to start extracting contacts one by one"
+ "STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\n"
+ "1. Open a web browser (Chrome or Firefox)\n"
+ "2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\n"
+ "3. Wait for the page to fully load\n"
+ "4. Confirm you can see the list of connections\n"
+ "5. Ready to start extracting contacts"
)
- print(f"\\n[Task 1/21] Navigating to LinkedIn connections page...")
+ print(f"\n[Task 1/21] Navigating to LinkedIn...")
history.append({"role": "user", "content": navigation_task})
async for result in agent.run(history, stream=False):
history += result.get("output", [])
- for item in result.get("output", []):
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
- print(f"ā
Navigation completed\\n")
+ print(f"ā
Navigation completed\n")
- # Tasks 2-21: Extract each of the 20 contacts
+ # Extract 20 contacts
contacts_extracted = 0
- linkedin_urls = [] # Track LinkedIn URLs for bonus messaging links
- previous_contact_name = None # Track the previous contact's name for easy navigation
+ linkedin_urls = []
+ previous_contact_name = None
for contact_num in range(1, 21):
- # Build extraction task based on whether this is the first contact or not
+ # Build extraction task
if contact_num == 1:
- # First contact - start from the top
extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Look at the very first connection at the top of the list\\n"
- f"2. Click on their name/profile link to open their LinkedIn profile page\\n"
- f"3. Wait for their profile page to load completely\\n"
- f"4. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"5. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"6. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"7. Do NOT add any extra text before or after these 5 lines\\n"
- f"8. Navigate back to the connections list page"
+ f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\n"
+ f"1. Click on the first connection's profile\n"
+ f"2. Extract: FIRST, LAST, ROLE, COMPANY, LINKEDIN URL\n"
+ f"3. Return in exact format:\n"
+ f"FIRST: [value]\n"
+ f"LAST: [value]\n"
+ f"ROLE: [value]\n"
+ f"COMPANY: [value]\n"
+ f"LINKEDIN: [value]\n"
+ f"4. Navigate back to connections list"
)
else:
- # Subsequent contacts - reference the previous contact
extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Find the contact named '{previous_contact_name}' in the list\\n"
- f"2. If you don't see '{previous_contact_name}' on the screen, scroll down slowly until you find them\\n"
- f"3. Once you find '{previous_contact_name}', look at the contact directly BELOW them\\n"
- f"4. Click on that contact's name/profile link (the one below '{previous_contact_name}') to open their profile page\\n"
- f"5. Wait for their profile page to load completely\\n"
- f"6. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"7. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"8. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"9. Do NOT add any extra text before or after these 5 lines\\n"
- f"10. Navigate back to the connections list page"
+ f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\n"
+ f"1. Find '{previous_contact_name}' in the list\n"
+ f"2. Click on the contact BELOW them\n"
+ f"3. Extract: FIRST, LAST, ROLE, COMPANY, LINKEDIN URL\n"
+ f"4. Return in exact format:\n"
+ f"FIRST: [value]\n"
+ f"LAST: [value]\n"
+ f"ROLE: [value]\n"
+ f"COMPANY: [value]\n"
+ f"LINKEDIN: [value]\n"
+ f"5. Navigate back"
)
print(f"[Task {contact_num + 1}/21] Extracting contact {contact_num}/20...")
history.append({"role": "user", "content": extraction_task})
- # Collect all output from the agent
all_output = []
async for result in agent.run(history, stream=False):
output = result.get("output", [])
history += output
all_output.extend(output)
- # Log agent output at debug level (only shown if verbosity increased)
- for item in output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- # Now extract contact information from ALL collected output (not just partial results)
contact_data = extract_contact_from_response(all_output)
- # Validate we got at least the critical fields (name or LinkedIn URL)
has_name = bool(contact_data['first'] and contact_data['last'])
has_linkedin = bool(contact_data['linkedin'] and 'linkedin.com' in contact_data['linkedin'])
- # Write to CSV if we got at least name OR linkedin
if has_name or has_linkedin:
with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
writer.writerow(contact_data)
contacts_extracted += 1
- # Track LinkedIn URL for messaging links
if contact_data['linkedin']:
linkedin_urls.append(contact_data['linkedin'])
- # Remember this contact's name for the next iteration
if has_name:
previous_contact_name = f"{contact_data['first']} {contact_data['last']}".strip()
- # Success message with what we got
name_str = f"{contact_data['first']} {contact_data['last']}" if has_name else "[No name]"
- linkedin_str = "ā LinkedIn" if has_linkedin else "ā No LinkedIn"
- role_str = f"({contact_data['role']})" if contact_data['role'] else "(No role)"
- print(f"ā
Contact {contact_num}/20 saved: {name_str} {role_str} | {linkedin_str}")
+ print(f"ā
Contact {contact_num}/20 saved: {name_str}")
else:
print(f"ā ļø Could not extract valid data for contact {contact_num}")
- print(f" Got: first='{contact_data['first']}', last='{contact_data['last']}', linkedin='{contact_data['linkedin'][:50] if contact_data['linkedin'] else 'None'}'")
- print(f" Check the agent's output above to see what was returned")
- print(f" Total output items: {len(all_output)}")
- # Progress update every 5 contacts
if contact_num % 5 == 0:
- print(f"\\nš Progress: {contacts_extracted}/{contact_num} contacts extracted so far...\\n")
+ print(f"\nš Progress: {contacts_extracted}/{contact_num} contacts extracted\n")
- # BONUS: Create messaging compose links file
+ # Create messaging links file
messaging_filename = f"linkedin_messaging_links_{timestamp}.txt"
messaging_path = os.path.join(os.getcwd(), messaging_filename)
with open(messaging_path, 'w', encoding='utf-8') as txtfile:
- txtfile.write("LinkedIn Messaging Compose Links\\n")
- txtfile.write("=" * 80 + "\\n\\n")
+ txtfile.write("LinkedIn Messaging Compose Links\n")
+ txtfile.write("=" * 80 + "\n\n")
for i, linkedin_url in enumerate(linkedin_urls, 1):
public_id = extract_public_id_from_linkedin_url(linkedin_url)
if public_id:
messaging_url = f"https://www.linkedin.com/messaging/compose/?recipient={public_id}"
- txtfile.write(f"{i}. {messaging_url}\\n")
- else:
- txtfile.write(f"{i}. [Could not extract public ID from: {linkedin_url}]\\n")
+ txtfile.write(f"{i}. {messaging_url}\n")
- print("\\n" + "="*80)
+ print("\n" + "="*80)
print("š All tasks completed!")
print(f"š CSV file saved to: {csv_path}")
print(f"š Total contacts extracted: {contacts_extracted}/20")
- print(f"š¬ Bonus: Messaging links saved to: {messaging_path}")
- print(f"š Total messaging links: {len(linkedin_urls)}")
+ print(f"š¬ Messaging links saved to: {messaging_path}")
print("="*80)
except Exception as e:
- print(f"\\nā Error during scraping: {e}")
+ print(f"\nā Error: {e}")
traceback.print_exc()
raise
def main():
-try:
-load_dotenv()
+ try:
+ load_dotenv()
if "ANTHROPIC_API_KEY" not in os.environ:
- raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
- "You can add it to a .env file in the project root."
- )
+ raise RuntimeError("Please set ANTHROPIC_API_KEY in .env")
if "CUA_API_KEY" not in os.environ:
- raise RuntimeError(
- "Please set the CUA_API_KEY environment variable.\\n"
- "You can add it to a .env file in the project root."
- )
+ raise RuntimeError("Please set CUA_API_KEY in .env")
+
+ if "CUA_CONTAINER_NAME" not in os.environ:
+ raise RuntimeError("Please set CUA_CONTAINER_NAME in .env")
signal.signal(signal.SIGINT, handle_sigint)
asyncio.run(scrape_linkedin_connections())
except Exception as e:
- print(f"\\nā Error running automation: {e}")
+ print(f"\nā Error: {e}")
traceback.print_exc()
-if **name** == "**main**":
-main()`}
-
-
+if __name__ == "__main__":
+ main()
+```
-
+
-
-{`import asyncio
-import csv
-import logging
-import os
-import signal
-import traceback
-from datetime import datetime
+```python
+# Same code as Cloud Sandbox, but change Computer initialization to:
+async with Computer(
+ os_type="linux",
+ provider_type=VMProviderType.DOCKER,
+ image="trycua/cua-xfce:latest",
+ verbosity=logging.INFO,
+) as computer:
+```
-from agent import ComputerAgent
-from computer import Computer, VMProviderType
-from dotenv import load_dotenv
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
-
-# Configuration: Define where you met these connections
-
-MET_AT_REASON = "`}{`"
-
-def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
-
-def extract_public_id_from_linkedin_url(linkedin_url):
-"""
-Extract public ID from LinkedIn profile URL.
-Example: https://www.linkedin.com/in/taylor-r-devries/?lipi=... -> taylor-r-devries
-"""
-if not linkedin_url:
-return None
-
- # Remove query parameters and trailing slashes
- url = linkedin_url.split('?')[0].rstrip('/')
-
- # Extract the part after /in/
- if '/in/' in url:
- public_id = url.split('/in/')[-1]
- return public_id
-
- return None
-
-def extract_contact_from_response(result_output):
-"""
-Extract contact information from agent's response.
-Expects the agent to return data in format:
-FIRST: value
-LAST: value
-ROLE: value
-COMPANY: value
-LINKEDIN: value
-
- Note: met_at is auto-filled from MET_AT_REASON constant.
- """
- contact = {
- 'first': '',
- 'last': '',
- 'role': '',
- 'company': '',
- 'met_at': MET_AT_REASON, # Auto-fill from constant
- 'linkedin': ''
- }
-
- # Collect all text from messages for debugging
- all_text = []
-
- for item in result_output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- text = content_part.get("text", "")
- if text:
- all_text.append(text)
- # Parse structured output - look for the exact format
- for line in text.split('\\n'):
- line = line.strip()
- # Use case-insensitive matching and handle extra whitespace
- line_upper = line.upper()
-
- if line_upper.startswith("FIRST:"):
- value = line[6:].strip() # Skip "FIRST:" prefix
- if value and value.upper() != "N/A":
- contact['first'] = value
- elif line_upper.startswith("LAST:"):
- value = line[5:].strip() # Skip "LAST:" prefix
- if value and value.upper() != "N/A":
- contact['last'] = value
- elif line_upper.startswith("ROLE:"):
- value = line[5:].strip() # Skip "ROLE:" prefix
- if value and value.upper() != "N/A":
- contact['role'] = value
- elif line_upper.startswith("COMPANY:"):
- value = line[8:].strip() # Skip "COMPANY:" prefix
- if value and value.upper() != "N/A":
- contact['company'] = value
- elif line_upper.startswith("LINKEDIN:"):
- value = line[9:].strip() # Skip "LINKEDIN:" prefix
- if value and value.upper() != "N/A":
- contact['linkedin'] = value
-
- # Debug logging
- if not (contact['first'] or contact['last'] or contact['linkedin']):
- logger.debug(f"Failed to extract. Full text content ({len(all_text)} messages):")
- for i, text in enumerate(all_text[-3:]): # Show last 3 messages
- logger.debug(f" Message {i}: {text[:200]}")
-
- return contact
-
-async def scrape_linkedin_connections():
-"""
-Scrape the first 20 connections from LinkedIn and export to CSV.
-The agent extracts data, and Python handles CSV writing programmatically.
-"""
-
- # Generate output filename with timestamp
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- csv_filename = f"linkedin_connections_{timestamp}.csv"
- csv_path = os.path.join(os.getcwd(), csv_filename)
-
- # Initialize CSV file with headers
- with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
- writer.writeheader()
-
- print(f"\\nš Starting LinkedIn connections scraper")
- print(f"š Output file: {csv_path}")
- print(f"š Met at: {MET_AT_REASON}")
- print("=" * 80)
-
- try:
- async with Computer(
- os_type="linux",
- provider_type=VMProviderType.DOCKER,
- name="`}{`",
- verbosity=logging.INFO,
- ) as computer:
-
- agent = ComputerAgent(
- model="anthropic/claude-sonnet-4-5-20250929",
- tools=[computer],
- only_n_most_recent_images=3,
- verbosity=logging.INFO,
- trajectory_dir="trajectories",
- use_prompt_caching=True,
- max_trajectory_budget=10.0,
- )
-
- history = []
-
- # Task 1: Navigate to LinkedIn connections page
- navigation_task = (
- "STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\\n"
- "1. Open a web browser (Chrome or Firefox)\\n"
- "2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\\n"
- "3. Wait for the page to fully load (look for the connection list to appear)\\n"
- "4. If prompted to log in, handle the authentication\\n"
- "5. Confirm you can see the list of connections displayed on the page\\n"
- "6. Ready to start extracting contacts one by one"
- )
-
- print(f"\\n[Task 1/21] Navigating to LinkedIn connections page...")
- history.append({"role": "user", "content": navigation_task})
-
- async for result in agent.run(history, stream=False):
- history += result.get("output", [])
- for item in result.get("output", []):
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- print(f"ā
Navigation completed\\n")
-
- # Tasks 2-21: Extract each of the 20 contacts
- contacts_extracted = 0
- linkedin_urls = [] # Track LinkedIn URLs for bonus messaging links
- previous_contact_name = None # Track the previous contact's name for easy navigation
-
- for contact_num in range(1, 21):
- # Build extraction task based on whether this is the first contact or not
- if contact_num == 1:
- # First contact - start from the top
- extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Look at the very first connection at the top of the list\\n"
- f"2. Click on their name/profile link to open their LinkedIn profile page\\n"
- f"3. Wait for their profile page to load completely\\n"
- f"4. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"5. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"6. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"7. Do NOT add any extra text before or after these 5 lines\\n"
- f"8. Navigate back to the connections list page"
- )
- else:
- # Subsequent contacts - reference the previous contact
- extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Find the contact named '{previous_contact_name}' in the list\\n"
- f"2. If you don't see '{previous_contact_name}' on the screen, scroll down slowly until you find them\\n"
- f"3. Once you find '{previous_contact_name}', look at the contact directly BELOW them\\n"
- f"4. Click on that contact's name/profile link (the one below '{previous_contact_name}') to open their profile page\\n"
- f"5. Wait for their profile page to load completely\\n"
- f"6. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"7. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"8. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"9. Do NOT add any extra text before or after these 5 lines\\n"
- f"10. Navigate back to the connections list page"
- )
-
- print(f"[Task {contact_num + 1}/21] Extracting contact {contact_num}/20...")
- history.append({"role": "user", "content": extraction_task})
-
- # Collect all output from the agent
- all_output = []
- async for result in agent.run(history, stream=False):
- output = result.get("output", [])
- history += output
- all_output.extend(output)
-
- # Log agent output at debug level (only shown if verbosity increased)
- for item in output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- # Now extract contact information from ALL collected output (not just partial results)
- contact_data = extract_contact_from_response(all_output)
-
- # Validate we got at least the critical fields (name or LinkedIn URL)
- has_name = bool(contact_data['first'] and contact_data['last'])
- has_linkedin = bool(contact_data['linkedin'] and 'linkedin.com' in contact_data['linkedin'])
-
- # Write to CSV if we got at least name OR linkedin
- if has_name or has_linkedin:
- with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
- writer.writerow(contact_data)
- contacts_extracted += 1
-
- # Track LinkedIn URL for messaging links
- if contact_data['linkedin']:
- linkedin_urls.append(contact_data['linkedin'])
-
- # Remember this contact's name for the next iteration
- if has_name:
- previous_contact_name = f"{contact_data['first']} {contact_data['last']}".strip()
-
- # Success message with what we got
- name_str = f"{contact_data['first']} {contact_data['last']}" if has_name else "[No name]"
- linkedin_str = "ā LinkedIn" if has_linkedin else "ā No LinkedIn"
- role_str = f"({contact_data['role']})" if contact_data['role'] else "(No role)"
- print(f"ā
Contact {contact_num}/20 saved: {name_str} {role_str} | {linkedin_str}")
- else:
- print(f"ā ļø Could not extract valid data for contact {contact_num}")
- print(f" Got: first='{contact_data['first']}', last='{contact_data['last']}', linkedin='{contact_data['linkedin'][:50] if contact_data['linkedin'] else 'None'}'")
- print(f" Check the agent's output above to see what was returned")
- print(f" Total output items: {len(all_output)}")
-
- # Progress update every 5 contacts
- if contact_num % 5 == 0:
- print(f"\\nš Progress: {contacts_extracted}/{contact_num} contacts extracted so far...\\n")
-
- # BONUS: Create messaging compose links file
- messaging_filename = f"linkedin_messaging_links_{timestamp}.txt"
- messaging_path = os.path.join(os.getcwd(), messaging_filename)
-
- with open(messaging_path, 'w', encoding='utf-8') as txtfile:
- txtfile.write("LinkedIn Messaging Compose Links\\n")
- txtfile.write("=" * 80 + "\\n\\n")
-
- for i, linkedin_url in enumerate(linkedin_urls, 1):
- public_id = extract_public_id_from_linkedin_url(linkedin_url)
- if public_id:
- messaging_url = f"https://www.linkedin.com/messaging/compose/?recipient={public_id}"
- txtfile.write(f"{i}. {messaging_url}\\n")
- else:
- txtfile.write(f"{i}. [Could not extract public ID from: {linkedin_url}]\\n")
-
- print("\\n" + "="*80)
- print("š All tasks completed!")
- print(f"š CSV file saved to: {csv_path}")
- print(f"š Total contacts extracted: {contacts_extracted}/20")
- print(f"š¬ Bonus: Messaging links saved to: {messaging_path}")
- print(f"š Total messaging links: {len(linkedin_urls)}")
- print("="*80)
-
- except Exception as e:
- print(f"\\nā Error during scraping: {e}")
- traceback.print_exc()
- raise
-
-def main():
-try:
-load_dotenv()
-
- if "ANTHROPIC_API_KEY" not in os.environ:
- raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
- "You can add it to a .env file in the project root."
- )
-
- signal.signal(signal.SIGINT, handle_sigint)
-
- asyncio.run(scrape_linkedin_connections())
-
- except Exception as e:
- print(f"\\nā Error running automation: {e}")
- traceback.print_exc()
-
-if **name** == "**main**":
-main()`}
-
-
+And remove the `CUA_API_KEY` and `CUA_CONTAINER_NAME` requirements from `.env` and the validation checks.
-
+
-
-{`import asyncio
-import csv
-import logging
-import os
-import signal
-import traceback
-from datetime import datetime
+```python
+# Same code as Cloud Sandbox, but change Computer initialization to:
+async with Computer(
+ os_type="macos",
+ provider_type=VMProviderType.LUME,
+ name="macos-sequoia-cua:latest",
+ verbosity=logging.INFO,
+) as computer:
+```
-from agent import ComputerAgent
-from computer import Computer, VMProviderType
-from dotenv import load_dotenv
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
-
-# Configuration: Define where you met these connections
-
-MET_AT_REASON = "`}{`"
-
-def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
-
-def extract_public_id_from_linkedin_url(linkedin_url):
-"""
-Extract public ID from LinkedIn profile URL.
-Example: https://www.linkedin.com/in/taylor-r-devries/?lipi=... -> taylor-r-devries
-"""
-if not linkedin_url:
-return None
-
- # Remove query parameters and trailing slashes
- url = linkedin_url.split('?')[0].rstrip('/')
-
- # Extract the part after /in/
- if '/in/' in url:
- public_id = url.split('/in/')[-1]
- return public_id
-
- return None
-
-def extract_contact_from_response(result_output):
-"""
-Extract contact information from agent's response.
-Expects the agent to return data in format:
-FIRST: value
-LAST: value
-ROLE: value
-COMPANY: value
-LINKEDIN: value
-
- Note: met_at is auto-filled from MET_AT_REASON constant.
- """
- contact = {
- 'first': '',
- 'last': '',
- 'role': '',
- 'company': '',
- 'met_at': MET_AT_REASON, # Auto-fill from constant
- 'linkedin': ''
- }
-
- # Collect all text from messages for debugging
- all_text = []
-
- for item in result_output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- text = content_part.get("text", "")
- if text:
- all_text.append(text)
- # Parse structured output - look for the exact format
- for line in text.split('\\n'):
- line = line.strip()
- # Use case-insensitive matching and handle extra whitespace
- line_upper = line.upper()
-
- if line_upper.startswith("FIRST:"):
- value = line[6:].strip() # Skip "FIRST:" prefix
- if value and value.upper() != "N/A":
- contact['first'] = value
- elif line_upper.startswith("LAST:"):
- value = line[5:].strip() # Skip "LAST:" prefix
- if value and value.upper() != "N/A":
- contact['last'] = value
- elif line_upper.startswith("ROLE:"):
- value = line[5:].strip() # Skip "ROLE:" prefix
- if value and value.upper() != "N/A":
- contact['role'] = value
- elif line_upper.startswith("COMPANY:"):
- value = line[8:].strip() # Skip "COMPANY:" prefix
- if value and value.upper() != "N/A":
- contact['company'] = value
- elif line_upper.startswith("LINKEDIN:"):
- value = line[9:].strip() # Skip "LINKEDIN:" prefix
- if value and value.upper() != "N/A":
- contact['linkedin'] = value
-
- # Debug logging
- if not (contact['first'] or contact['last'] or contact['linkedin']):
- logger.debug(f"Failed to extract. Full text content ({len(all_text)} messages):")
- for i, text in enumerate(all_text[-3:]): # Show last 3 messages
- logger.debug(f" Message {i}: {text[:200]}")
-
- return contact
-
-async def scrape_linkedin_connections():
-"""
-Scrape the first 20 connections from LinkedIn and export to CSV.
-The agent extracts data, and Python handles CSV writing programmatically.
-"""
-
- # Generate output filename with timestamp
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- csv_filename = f"linkedin_connections_{timestamp}.csv"
- csv_path = os.path.join(os.getcwd(), csv_filename)
-
- # Initialize CSV file with headers
- with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
- writer.writeheader()
-
- print(f"\\nš Starting LinkedIn connections scraper")
- print(f"š Output file: {csv_path}")
- print(f"š Met at: {MET_AT_REASON}")
- print("=" * 80)
-
- try:
- async with Computer(
- os_type="macos",
- provider_type=VMProviderType.LUME,
- name="`}{`",
- verbosity=logging.INFO,
- ) as computer:
-
- agent = ComputerAgent(
- model="anthropic/claude-sonnet-4-5-20250929",
- tools=[computer],
- only_n_most_recent_images=3,
- verbosity=logging.INFO,
- trajectory_dir="trajectories",
- use_prompt_caching=True,
- max_trajectory_budget=10.0,
- )
-
- history = []
-
- # Task 1: Navigate to LinkedIn connections page
- navigation_task = (
- "STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\\n"
- "1. Open a web browser (Chrome or Firefox)\\n"
- "2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\\n"
- "3. Wait for the page to fully load (look for the connection list to appear)\\n"
- "4. If prompted to log in, handle the authentication\\n"
- "5. Confirm you can see the list of connections displayed on the page\\n"
- "6. Ready to start extracting contacts one by one"
- )
-
- print(f"\\n[Task 1/21] Navigating to LinkedIn connections page...")
- history.append({"role": "user", "content": navigation_task})
-
- async for result in agent.run(history, stream=False):
- history += result.get("output", [])
- for item in result.get("output", []):
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- print(f"ā
Navigation completed\\n")
-
- # Tasks 2-21: Extract each of the 20 contacts
- contacts_extracted = 0
- linkedin_urls = [] # Track LinkedIn URLs for bonus messaging links
- previous_contact_name = None # Track the previous contact's name for easy navigation
-
- for contact_num in range(1, 21):
- # Build extraction task based on whether this is the first contact or not
- if contact_num == 1:
- # First contact - start from the top
- extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Look at the very first connection at the top of the list\\n"
- f"2. Click on their name/profile link to open their LinkedIn profile page\\n"
- f"3. Wait for their profile page to load completely\\n"
- f"4. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"5. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"6. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"7. Do NOT add any extra text before or after these 5 lines\\n"
- f"8. Navigate back to the connections list page"
- )
- else:
- # Subsequent contacts - reference the previous contact
- extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Find the contact named '{previous_contact_name}' in the list\\n"
- f"2. If you don't see '{previous_contact_name}' on the screen, scroll down slowly until you find them\\n"
- f"3. Once you find '{previous_contact_name}', look at the contact directly BELOW them\\n"
- f"4. Click on that contact's name/profile link (the one below '{previous_contact_name}') to open their profile page\\n"
- f"5. Wait for their profile page to load completely\\n"
- f"6. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"7. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"8. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"9. Do NOT add any extra text before or after these 5 lines\\n"
- f"10. Navigate back to the connections list page"
- )
-
- print(f"[Task {contact_num + 1}/21] Extracting contact {contact_num}/20...")
- history.append({"role": "user", "content": extraction_task})
-
- # Collect all output from the agent
- all_output = []
- async for result in agent.run(history, stream=False):
- output = result.get("output", [])
- history += output
- all_output.extend(output)
-
- # Log agent output at debug level (only shown if verbosity increased)
- for item in output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- # Now extract contact information from ALL collected output (not just partial results)
- contact_data = extract_contact_from_response(all_output)
-
- # Validate we got at least the critical fields (name or LinkedIn URL)
- has_name = bool(contact_data['first'] and contact_data['last'])
- has_linkedin = bool(contact_data['linkedin'] and 'linkedin.com' in contact_data['linkedin'])
-
- # Write to CSV if we got at least name OR linkedin
- if has_name or has_linkedin:
- with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
- writer.writerow(contact_data)
- contacts_extracted += 1
-
- # Track LinkedIn URL for messaging links
- if contact_data['linkedin']:
- linkedin_urls.append(contact_data['linkedin'])
-
- # Remember this contact's name for the next iteration
- if has_name:
- previous_contact_name = f"{contact_data['first']} {contact_data['last']}".strip()
-
- # Success message with what we got
- name_str = f"{contact_data['first']} {contact_data['last']}" if has_name else "[No name]"
- linkedin_str = "ā LinkedIn" if has_linkedin else "ā No LinkedIn"
- role_str = f"({contact_data['role']})" if contact_data['role'] else "(No role)"
- print(f"ā
Contact {contact_num}/20 saved: {name_str} {role_str} | {linkedin_str}")
- else:
- print(f"ā ļø Could not extract valid data for contact {contact_num}")
- print(f" Got: first='{contact_data['first']}', last='{contact_data['last']}', linkedin='{contact_data['linkedin'][:50] if contact_data['linkedin'] else 'None'}'")
- print(f" Check the agent's output above to see what was returned")
- print(f" Total output items: {len(all_output)}")
-
- # Progress update every 5 contacts
- if contact_num % 5 == 0:
- print(f"\\nš Progress: {contacts_extracted}/{contact_num} contacts extracted so far...\\n")
-
- # BONUS: Create messaging compose links file
- messaging_filename = f"linkedin_messaging_links_{timestamp}.txt"
- messaging_path = os.path.join(os.getcwd(), messaging_filename)
-
- with open(messaging_path, 'w', encoding='utf-8') as txtfile:
- txtfile.write("LinkedIn Messaging Compose Links\\n")
- txtfile.write("=" * 80 + "\\n\\n")
-
- for i, linkedin_url in enumerate(linkedin_urls, 1):
- public_id = extract_public_id_from_linkedin_url(linkedin_url)
- if public_id:
- messaging_url = f"https://www.linkedin.com/messaging/compose/?recipient={public_id}"
- txtfile.write(f"{i}. {messaging_url}\\n")
- else:
- txtfile.write(f"{i}. [Could not extract public ID from: {linkedin_url}]\\n")
-
- print("\\n" + "="*80)
- print("š All tasks completed!")
- print(f"š CSV file saved to: {csv_path}")
- print(f"š Total contacts extracted: {contacts_extracted}/20")
- print(f"š¬ Bonus: Messaging links saved to: {messaging_path}")
- print(f"š Total messaging links: {len(linkedin_urls)}")
- print("="*80)
-
- except Exception as e:
- print(f"\\nā Error during scraping: {e}")
- traceback.print_exc()
- raise
-
-def main():
-try:
-load_dotenv()
-
- if "ANTHROPIC_API_KEY" not in os.environ:
- raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
- "You can add it to a .env file in the project root."
- )
-
- signal.signal(signal.SIGINT, handle_sigint)
-
- asyncio.run(scrape_linkedin_connections())
-
- except Exception as e:
- print(f"\\nā Error running automation: {e}")
- traceback.print_exc()
-
-if **name** == "**main**":
-main()`}
-
-
+And remove the `CUA_API_KEY` and `CUA_CONTAINER_NAME` requirements from `.env` and the validation checks.
-
+
-
-{`import asyncio
-import csv
-import logging
-import os
-import signal
-import traceback
-from datetime import datetime
+```python
+# Same code as Cloud Sandbox, but change Computer initialization to:
+async with Computer(
+ os_type="windows",
+ provider_type=VMProviderType.WINDOWS_SANDBOX,
+ verbosity=logging.INFO,
+) as computer:
+```
-from agent import ComputerAgent
-from computer import Computer, VMProviderType
-from dotenv import load_dotenv
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(**name**)
-
-# Configuration: Define where you met these connections
-
-MET_AT_REASON = "`}{`"
-
-def handle_sigint(sig, frame):
-print("\\n\\nExecution interrupted by user. Exiting gracefully...")
-exit(0)
-
-def extract_public_id_from_linkedin_url(linkedin_url):
-"""
-Extract public ID from LinkedIn profile URL.
-Example: https://www.linkedin.com/in/taylor-r-devries/?lipi=... -> taylor-r-devries
-"""
-if not linkedin_url:
-return None
-
- # Remove query parameters and trailing slashes
- url = linkedin_url.split('?')[0].rstrip('/')
-
- # Extract the part after /in/
- if '/in/' in url:
- public_id = url.split('/in/')[-1]
- return public_id
-
- return None
-
-def extract_contact_from_response(result_output):
-"""
-Extract contact information from agent's response.
-Expects the agent to return data in format:
-FIRST: value
-LAST: value
-ROLE: value
-COMPANY: value
-LINKEDIN: value
-
- Note: met_at is auto-filled from MET_AT_REASON constant.
- """
- contact = {
- 'first': '',
- 'last': '',
- 'role': '',
- 'company': '',
- 'met_at': MET_AT_REASON, # Auto-fill from constant
- 'linkedin': ''
- }
-
- # Collect all text from messages for debugging
- all_text = []
-
- for item in result_output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- text = content_part.get("text", "")
- if text:
- all_text.append(text)
- # Parse structured output - look for the exact format
- for line in text.split('\\n'):
- line = line.strip()
- # Use case-insensitive matching and handle extra whitespace
- line_upper = line.upper()
-
- if line_upper.startswith("FIRST:"):
- value = line[6:].strip() # Skip "FIRST:" prefix
- if value and value.upper() != "N/A":
- contact['first'] = value
- elif line_upper.startswith("LAST:"):
- value = line[5:].strip() # Skip "LAST:" prefix
- if value and value.upper() != "N/A":
- contact['last'] = value
- elif line_upper.startswith("ROLE:"):
- value = line[5:].strip() # Skip "ROLE:" prefix
- if value and value.upper() != "N/A":
- contact['role'] = value
- elif line_upper.startswith("COMPANY:"):
- value = line[8:].strip() # Skip "COMPANY:" prefix
- if value and value.upper() != "N/A":
- contact['company'] = value
- elif line_upper.startswith("LINKEDIN:"):
- value = line[9:].strip() # Skip "LINKEDIN:" prefix
- if value and value.upper() != "N/A":
- contact['linkedin'] = value
-
- # Debug logging
- if not (contact['first'] or contact['last'] or contact['linkedin']):
- logger.debug(f"Failed to extract. Full text content ({len(all_text)} messages):")
- for i, text in enumerate(all_text[-3:]): # Show last 3 messages
- logger.debug(f" Message {i}: {text[:200]}")
-
- return contact
-
-async def scrape_linkedin_connections():
-"""
-Scrape the first 20 connections from LinkedIn and export to CSV.
-The agent extracts data, and Python handles CSV writing programmatically.
-"""
-
- # Generate output filename with timestamp
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- csv_filename = f"linkedin_connections_{timestamp}.csv"
- csv_path = os.path.join(os.getcwd(), csv_filename)
-
- # Initialize CSV file with headers
- with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
- writer.writeheader()
-
- print(f"\\nš Starting LinkedIn connections scraper")
- print(f"š Output file: {csv_path}")
- print(f"š Met at: {MET_AT_REASON}")
- print("=" * 80)
-
- try:
- async with Computer(
- os_type="windows",
- provider_type=VMProviderType.WINDOWS_SANDBOX,
- verbosity=logging.INFO,
- ) as computer:
-
- agent = ComputerAgent(
- model="anthropic/claude-sonnet-4-5-20250929",
- tools=[computer],
- only_n_most_recent_images=3,
- verbosity=logging.INFO,
- trajectory_dir="trajectories",
- use_prompt_caching=True,
- max_trajectory_budget=10.0,
- )
-
- history = []
-
- # Task 1: Navigate to LinkedIn connections page
- navigation_task = (
- "STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\\n"
- "1. Open a web browser (Chrome or Firefox)\\n"
- "2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\\n"
- "3. Wait for the page to fully load (look for the connection list to appear)\\n"
- "4. If prompted to log in, handle the authentication\\n"
- "5. Confirm you can see the list of connections displayed on the page\\n"
- "6. Ready to start extracting contacts one by one"
- )
-
- print(f"\\n[Task 1/21] Navigating to LinkedIn connections page...")
- history.append({"role": "user", "content": navigation_task})
-
- async for result in agent.run(history, stream=False):
- history += result.get("output", [])
- for item in result.get("output", []):
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- print(f"ā
Navigation completed\\n")
-
- # Tasks 2-21: Extract each of the 20 contacts
- contacts_extracted = 0
- linkedin_urls = [] # Track LinkedIn URLs for bonus messaging links
- previous_contact_name = None # Track the previous contact's name for easy navigation
-
- for contact_num in range(1, 21):
- # Build extraction task based on whether this is the first contact or not
- if contact_num == 1:
- # First contact - start from the top
- extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Look at the very first connection at the top of the list\\n"
- f"2. Click on their name/profile link to open their LinkedIn profile page\\n"
- f"3. Wait for their profile page to load completely\\n"
- f"4. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"5. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"6. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"7. Do NOT add any extra text before or after these 5 lines\\n"
- f"8. Navigate back to the connections list page"
- )
- else:
- # Subsequent contacts - reference the previous contact
- extraction_task = (
- f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\\n"
- f"1. Find the contact named '{previous_contact_name}' in the list\\n"
- f"2. If you don't see '{previous_contact_name}' on the screen, scroll down slowly until you find them\\n"
- f"3. Once you find '{previous_contact_name}', look at the contact directly BELOW them\\n"
- f"4. Click on that contact's name/profile link (the one below '{previous_contact_name}') to open their profile page\\n"
- f"5. Wait for their profile page to load completely\\n"
- f"6. Extract the following information from their profile:\\n"
- f" - First name: Extract from their display name at the top (just the first name)\\n"
- f" - Last name: Extract from their display name at the top (just the last name)\\n"
- f" - Current role/title: Extract from the HEADLINE directly under their name (e.g., 'Software Engineer')\\n"
- f" - Company name: Extract from the HEADLINE (typically after 'at' or '@', e.g., 'Software Engineer at Google' ā 'Google')\\n"
- f" - LinkedIn profile URL: Copy the FULL URL from the browser address bar (must start with https://www.linkedin.com/in/)\\n"
- f"7. CRITICAL: You MUST return ALL 5 fields in this EXACT format with each field on its own line:\\n"
- f"FIRST: [first name]\\n"
- f"LAST: [last name]\\n"
- f"ROLE: [role/title from headline]\\n"
- f"COMPANY: [company from headline]\\n"
- f"LINKEDIN: [full profile URL]\\n"
- f"\\n"
- f"8. If any field is not available, write 'N/A' instead of leaving it blank\\n"
- f"9. Do NOT add any extra text before or after these 5 lines\\n"
- f"10. Navigate back to the connections list page"
- )
-
- print(f"[Task {contact_num + 1}/21] Extracting contact {contact_num}/20...")
- history.append({"role": "user", "content": extraction_task})
-
- # Collect all output from the agent
- all_output = []
- async for result in agent.run(history, stream=False):
- output = result.get("output", [])
- history += output
- all_output.extend(output)
-
- # Log agent output at debug level (only shown if verbosity increased)
- for item in output:
- if item.get("type") == "message":
- content = item.get("content", [])
- for content_part in content:
- if content_part.get("text"):
- logger.debug(f"Agent: {content_part.get('text')}")
-
- # Now extract contact information from ALL collected output (not just partial results)
- contact_data = extract_contact_from_response(all_output)
-
- # Validate we got at least the critical fields (name or LinkedIn URL)
- has_name = bool(contact_data['first'] and contact_data['last'])
- has_linkedin = bool(contact_data['linkedin'] and 'linkedin.com' in contact_data['linkedin'])
-
- # Write to CSV if we got at least name OR linkedin
- if has_name or has_linkedin:
- with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
- writer.writerow(contact_data)
- contacts_extracted += 1
-
- # Track LinkedIn URL for messaging links
- if contact_data['linkedin']:
- linkedin_urls.append(contact_data['linkedin'])
-
- # Remember this contact's name for the next iteration
- if has_name:
- previous_contact_name = f"{contact_data['first']} {contact_data['last']}".strip()
-
- # Success message with what we got
- name_str = f"{contact_data['first']} {contact_data['last']}" if has_name else "[No name]"
- linkedin_str = "ā LinkedIn" if has_linkedin else "ā No LinkedIn"
- role_str = f"({contact_data['role']})" if contact_data['role'] else "(No role)"
- print(f"ā
Contact {contact_num}/20 saved: {name_str} {role_str} | {linkedin_str}")
- else:
- print(f"ā ļø Could not extract valid data for contact {contact_num}")
- print(f" Got: first='{contact_data['first']}', last='{contact_data['last']}', linkedin='{contact_data['linkedin'][:50] if contact_data['linkedin'] else 'None'}'")
- print(f" Check the agent's output above to see what was returned")
- print(f" Total output items: {len(all_output)}")
-
- # Progress update every 5 contacts
- if contact_num % 5 == 0:
- print(f"\\nš Progress: {contacts_extracted}/{contact_num} contacts extracted so far...\\n")
-
- # BONUS: Create messaging compose links file
- messaging_filename = f"linkedin_messaging_links_{timestamp}.txt"
- messaging_path = os.path.join(os.getcwd(), messaging_filename)
-
- with open(messaging_path, 'w', encoding='utf-8') as txtfile:
- txtfile.write("LinkedIn Messaging Compose Links\\n")
- txtfile.write("=" * 80 + "\\n\\n")
-
- for i, linkedin_url in enumerate(linkedin_urls, 1):
- public_id = extract_public_id_from_linkedin_url(linkedin_url)
- if public_id:
- messaging_url = f"https://www.linkedin.com/messaging/compose/?recipient={public_id}"
- txtfile.write(f"{i}. {messaging_url}\\n")
- else:
- txtfile.write(f"{i}. [Could not extract public ID from: {linkedin_url}]\\n")
-
- print("\\n" + "="*80)
- print("š All tasks completed!")
- print(f"š CSV file saved to: {csv_path}")
- print(f"š Total contacts extracted: {contacts_extracted}/20")
- print(f"š¬ Bonus: Messaging links saved to: {messaging_path}")
- print(f"š Total messaging links: {len(linkedin_urls)}")
- print("="*80)
-
- except Exception as e:
- print(f"\\nā Error during scraping: {e}")
- traceback.print_exc()
- raise
-
-def main():
-try:
-load_dotenv()
-
- if "ANTHROPIC_API_KEY" not in os.environ:
- raise RuntimeError(
- "Please set the ANTHROPIC_API_KEY environment variable.\\n"
- "You can add it to a .env file in the project root."
- )
-
- signal.signal(signal.SIGINT, handle_sigint)
-
- asyncio.run(scrape_linkedin_connections())
-
- except Exception as e:
- print(f"\\nā Error running automation: {e}")
- traceback.print_exc()
-
-if **name** == "**main**":
-main()`}
-
-
+And remove the `CUA_API_KEY` and `CUA_CONTAINER_NAME` requirements from `.env` and the validation checks.
+
+
+
+
+### Run Your Script
+
+Execute your contact extraction automation:
+
+```bash
+python contact_export.py
+```
+
+The agent will:
+1. Navigate to your LinkedIn connections page
+2. Extract data from 20 contacts (first name, last name, role, company, LinkedIn URL)
+3. Save contacts to a timestamped CSV file
+4. Generate messaging compose links for easy follow-up
+
+Monitor the output to see the agent's progress. The script will show a progress update every 5 contacts.
+
+
+
+
+
+---
+
## How It Works
This script demonstrates a practical workflow for extracting LinkedIn connection data:
-1. **Session Persistence** - Manually log into LinkedIn through the VM once, and the VM saves your session so the agent appears as your regular browsing.
-2. **Navigation** - The script navigates to your LinkedIn connections page using your saved authenticated session.
-3. **Data Extraction** - For each contact, the agent clicks their profile, extracts name/role/company/URL, and navigates back to repeat.
-4. **Python Processing** - Python parses the agent's responses, validates data, and writes to CSV incrementally to preserve progress.
-5. **Output Files** - Generates a CSV with contact data and a text file with direct messaging URLs.
+1. **Session Persistence** - Manually log into LinkedIn through the VM once, and the VM saves your session
+2. **Navigation** - The script navigates to your connections page using your saved authenticated session
+3. **Data Extraction** - For each contact, the agent clicks their profile, extracts data, and navigates back
+4. **Python Processing** - Python parses responses, validates data, and writes to CSV incrementally
+5. **Output Files** - Generates a CSV with contact data and a text file with messaging URLs
## Next Steps
@@ -1521,3 +470,4 @@ This script demonstrates a practical workflow for extracting LinkedIn connection
- Read about [Agent loops](/agent-sdk/agent-loops), [tools](/agent-sdk/custom-tools), and [supported model providers](/agent-sdk/supported-model-providers/)
- Experiment with different [Models and Providers](/agent-sdk/supported-model-providers/)
- Adapt this script for other platforms (Twitter/X, email extraction, etc.)
+- Join our [Discord community](https://discord.com/invite/mVnXXpdE85) for help
diff --git a/docs/content/docs/get-started/meta.json b/docs/content/docs/get-started/meta.json
index f7f9fac2..a14e8acb 100644
--- a/docs/content/docs/get-started/meta.json
+++ b/docs/content/docs/get-started/meta.json
@@ -3,5 +3,5 @@
"description": "Get started with Cua",
"defaultOpen": true,
"icon": "Rocket",
- "pages": ["quickstart"]
+ "pages": ["../index", "quickstart"]
}
diff --git a/docs/content/docs/get-started/quickstart.mdx b/docs/content/docs/get-started/quickstart.mdx
index e0b09980..23f47085 100644
--- a/docs/content/docs/get-started/quickstart.mdx
+++ b/docs/content/docs/get-started/quickstart.mdx
@@ -8,7 +8,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Code, Terminal } from 'lucide-react';
-Choose your quickstart path:
+{/* Choose your quickstart path:
} href="#developer-quickstart" title="Developer Quickstart">
@@ -17,7 +17,7 @@ Choose your quickstart path:
} href="#cli-quickstart" title="CLI Quickstart">
Get started quickly with the command-line interface
-
+ */}
---
@@ -30,11 +30,11 @@ You can run your Cua computer in the cloud (recommended for easiest setup), loca
- Cua Cloud Sandbox provides sandboxes that run Linux (Ubuntu) or Windows.
+ Cua Cloud Sandbox provides sandboxes that run Linux (Ubuntu), Windows, or macOS.
1. Go to [cua.ai/signin](https://cua.ai/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
- 3. Create a **Small** sandbox, choosing either **Linux** or **Windows**
+ 3. Create a **Small** sandbox, choosing **Linux**, **Windows**, or **macOS**
4. Note your sandbox name and API key
Your Cloud Sandbox will be automatically configured and ready to use.
@@ -117,7 +117,7 @@ Connect to your Cua computer and perform basic interactions, such as taking scre
from computer import Computer
computer = Computer(
- os_type="linux",
+ os_type="linux", # or "windows" or "macos"
provider_type="cloud",
name="your-sandbox-name",
api_key="your-api-key"
@@ -192,6 +192,10 @@ Connect to your Cua computer and perform basic interactions, such as taking scre
+
+ The TypeScript interface is currently deprecated. We're working on version 0.2.0 with improved TypeScript support. In the meantime, please use the Python SDK.
+
+
Install the Cua computer TypeScript SDK:
```bash
npm install @trycua/computer
@@ -205,7 +209,7 @@ Connect to your Cua computer and perform basic interactions, such as taking scre
import { Computer, OSType } from '@trycua/computer';
const computer = new Computer({
- osType: OSType.LINUX,
+ osType: OSType.LINUX, // or OSType.WINDOWS or OSType.MACOS
name: "your-sandbox-name",
apiKey: "your-api-key"
});
@@ -328,7 +332,7 @@ Learn more about agents in [Agent Loops](/agent-sdk/agent-loops) and available m
- Join our [Discord community](https://discord.com/invite/mVnXXpdE85) for help
- Try out [Form Filling](/example-usecases/form-filling) preset usecase
----
+{/* ---
## CLI Quickstart
@@ -354,7 +358,7 @@ Get started quickly with the CUA CLI - the easiest way to manage cloud sandboxes
```bash
# Install Bun if you don't have it
curl -fsSL https://bun.sh/install | bash
-
+
# Install CUA CLI
bun add -g @trycua/cli
```
@@ -467,4 +471,4 @@ cua delete my-vm-abc123
---
-For running models locally, see [Running Models Locally](/agent-sdk/supported-model-providers/local-models).
+For running models locally, see [Running Models Locally](/agent-sdk/supported-model-providers/local-models). */}
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
index 9c47c293..acecca6d 100644
--- a/docs/content/docs/index.mdx
+++ b/docs/content/docs/index.mdx
@@ -4,15 +4,9 @@ title: Introduction
import { Monitor, Code, BookOpen, Zap, Bot, Boxes, Rocket } from 'lucide-react';
-
-
+
Cua is an open-source framework for building **Computer-Use Agents** - AI systems that see, understand, and interact with desktop applications through vision and action, just like humans do.
-
-
-
-Go from prototype to production with everything you need: multi-provider LLM support, cross-platform sandboxes, and trajectory tracing. Whether you're running locally or deploying to the cloud, Cua gives you the tools to build reliable computer-use agents.
-
-
+
## Why Cua?
@@ -46,14 +40,14 @@ Follow the [Quickstart guide](/docs/get-started/quickstart) for step-by-step set
If you're new to computer-use agents, check out our [tutorials](https://cua.ai/blog), [examples](https://github.com/trycua/cua/tree/main/examples), and [notebooks](https://github.com/trycua/cua/tree/main/notebooks) to start building with Cua today.
-
} href="/docs/get-started/quickstart" title="Quickstart">
+
} href="/get-started/quickstart" title="Quickstart">
Get up and running in 3 steps with Python or TypeScript.
-
} href="/agent-sdk/agent-loops" title="Learn Core Concepts">
- Understand agent loops, callbacks, and model composition.
+
} href="/agent-sdk/agent-loops" title="Agent Loops">
+ Learn how agents work and how to build your own.
-
} href="/libraries/agent" title="API Reference">
- Explore the full Agent SDK and Computer SDK APIs.
+
} href="/computer-sdk/computers" title="Computer SDK">
+ Control desktop applications with the Computer SDK.
} href="/example-usecases/form-filling" title="Example Use Cases">
See Cua in action with real-world examples.
diff --git a/docs/content/docs/libraries/computer-server/index.mdx b/docs/content/docs/libraries/computer-server/index.mdx
index d5affd25..e2f683dd 100644
--- a/docs/content/docs/libraries/computer-server/index.mdx
+++ b/docs/content/docs/libraries/computer-server/index.mdx
@@ -7,14 +7,7 @@ github:
---
- A corresponding{' '}
-
- Jupyter Notebook
- {' '}
- is available for this documentation.
+ A corresponding Jupyter Notebook is available for this documentation.
The Computer Server API reference documentation is currently under development.
diff --git a/docs/content/docs/libraries/som/index.mdx b/docs/content/docs/libraries/som/index.mdx
index 3eef53f1..7a210290 100644
--- a/docs/content/docs/libraries/som/index.mdx
+++ b/docs/content/docs/libraries/som/index.mdx
@@ -7,11 +7,7 @@ github:
---
- A corresponding{' '}
-
- Python example
- {' '}
- is available for this documentation.
+ A corresponding Python example is available for this documentation.
## Overview
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
index 30e90eb3..199556f1 100644
--- a/docs/content/docs/meta.json
+++ b/docs/content/docs/meta.json
@@ -4,7 +4,6 @@
"root": true,
"defaultOpen": true,
"pages": [
- "index",
"---[Rocket]Get Started---",
"...get-started",
"---[ChefHat]Cookbook---",
diff --git a/docs/src/app/layout.config.tsx b/docs/src/app/layout.config.tsx
index 6d8e9e38..f47250c5 100644
--- a/docs/src/app/layout.config.tsx
+++ b/docs/src/app/layout.config.tsx
@@ -37,6 +37,7 @@ export const baseOptions: BaseLayoutProps = {
Cua
>
),
+ url: 'https://cua.ai',
},
githubUrl: 'https://github.com/trycua/cua',
links: [