From 2b595f5de8509a2917172f5e73b6fac4ab14b990 Mon Sep 17 00:00:00 2001 From: f-trycua Date: Mon, 17 Nov 2025 16:56:17 +0100 Subject: [PATCH] Fixes pre-launch week --- docs/content/docs/agent-sdk/agent-loops.mdx | 6 +- .../agent-sdk/customizing-computeragent.mdx | 11 +- .../docs/agent-sdk/integrations/hud.mdx | 6 +- docs/content/docs/agent-sdk/meta.json | 2 - .../content/docs/computer-sdk/computer-ui.mdx | 6 +- .../custom-computer-handlers.mdx | 0 docs/content/docs/computer-sdk/meta.json | 2 +- .../docs/computer-sdk/sandboxed-python.mdx | 9 +- .../docs/example-usecases/form-filling.mdx | 253 +-- .../post-event-contact-export.mdx | 1410 +++-------------- docs/content/docs/get-started/meta.json | 2 +- docs/content/docs/get-started/quickstart.mdx | 22 +- docs/content/docs/index.mdx | 20 +- .../docs/libraries/computer-server/index.mdx | 9 +- docs/content/docs/libraries/som/index.mdx | 6 +- docs/content/docs/meta.json | 1 - docs/src/app/layout.config.tsx | 1 + 17 files changed, 348 insertions(+), 1418 deletions(-) rename docs/content/docs/{agent-sdk => computer-sdk}/custom-computer-handlers.mdx (100%) 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: [