# A3 Project Template

Create and scaffold A3 automation workflows.

> **Note:** Always use the latest versions of `@athree/runner` and `@athree/module-loader`.
> You can find the latest versions on npm:
> - [@athree/runner](https://www.npmjs.com/package/@athree/runner)
> - [@athree/module-loader](https://www.npmjs.com/package/@athree/module-loader)
>
> Or check the latest release commits at https://github.com/ubio/athree/commits/main/

## Create a New Project

```bash
npx @athree/create-project a3-project-starter
cd a3-project-starter
npm i && npm run compile
```

**Naming convention:** Projects should be named `a3-project-{name}` (e.g., `a3-project-hotel-rates`).

> **Note:** You must run `npm run compile` for the project to appear in your local athree runner.

Or clone manually:

```bash
cp -r a3-project-template a3-project-starter
cd a3-project-starter
npm i && npm run compile
```

## Adding Project to Runner

Use the **Add Project** button in the A3 runner UI to add your project directory. The runner will automatically detect workflows in compiled projects.

## Environment Setup

Configure your environment variables before running workflows:

```bash
cp .env.example .env
# Edit .env and add your API keys
```

**Required variables:**
- At least one LLM API key: `GOOGLEAI_API_KEY`, `OPENAI_API_KEY`, or `ANTHROPIC_API_KEY`

**Optional variables:**
- `CHROME_HEADLESS` — Run browser in headless mode (default: `true`)
- `CHROME_KEEP_OPEN` — Keep browser open between runs (default: `true`)
- `CHROME_PERSISTENT` — Use persistent browser profile (default: `false`). Required for extensions. See [Chrome Extensions](#chrome-extensions).
- `EXTENSIONS_CONFIG_PATH` — Path to your `extensions.json` file. See [Chrome Extensions](#chrome-extensions).

## Chrome Extensions

Browser extensions (e.g. captcha solvers, cookie consent handlers) can be automatically installed into the Chrome profile before your workflow runs. This requires four steps:

### Step 1: Install the dependency

```bash
npm install @athree/runner-extensions
```

### Step 2: Create the extensions directory and config

This template ships with the `idcac` (I don't care about cookies) extension pre-installed in `chrome-extensions/`. To add more extensions, copy unpacked extension directories from `a3-rate-shopper/chrome-extensions/` or other projects and add entries to `extensions.json`.

```
├── chrome-extensions/
│   ├── extensions.json          # Extension manifest
│   ├── idcac/                   # Pre-installed: cookie consent automation
│   └── {your-extension}/        # Add more as needed
```

Each extension must be an **unpacked Chrome extension** (a directory containing a `manifest.json`). You can get these by:
- Copying from an existing project (e.g. `a3-rate-shopper/chrome-extensions/`)
- Downloading a `.crx` file and extracting it
- Cloning the extension source from GitHub

The `extensions.json` file lists which extensions to load:

```json
{
    "extensions": [
        {
            "name": "idcac",
            "path": "./chrome-extensions/idcac",
            "description": "I don't care about cookies - Cookie consent automation",
            "enabled": true,
            "loadInPersistent": true,
            "loadInTransient": true
        }
    ]
}
```

To add a captcha solver, copy the directory and add an entry:

```json
{
    "name": "capsolver",
    "path": "./chrome-extensions/capsolver",
    "description": "CapSolver - Automated captcha solving extension",
    "enabled": true,
    "loadInPersistent": true,
    "loadInTransient": false
}
```

**Fields:**
- `name` — Human-readable identifier
- `path` — Relative path from the project root to the unpacked extension directory
- `description` — What the extension does
- `enabled` — Set to `false` to skip without removing the entry
- `loadInPersistent` — Load when the browser runs in persistent mode (`CHROME_PERSISTENT=true`)
- `loadInTransient` — Load when the browser runs in transient mode (default)

### Step 3: Set environment variables

Add these to your `.env` file:

```bash
CHROME_PERSISTENT=true
EXTENSIONS_CONFIG_PATH=./chrome-extensions/extensions.json
```

> **Important:** Most captcha solver extensions set `loadInPersistent: true` and `loadInTransient: false`, so `CHROME_PERSISTENT=true` is required for them to load.

### Step 4: Call `installExtensions()` in your workflow

In your workflow's `run()` method, register the service and install extensions **before** calling `getPage()`:

```typescript
import { BrowserService, workflow } from '@athree/runner';
import { BrowserExtensionsService } from '@athree/runner-extensions';
import { dep, Mesh } from 'mesh-ioc';

@workflow({ title: 'My Workflow' })
export class MyWorkflow {
    @dep() private browserService!: BrowserService;
    @dep() private browserExtensionsService!: BrowserExtensionsService;
    @dep() private mesh!: Mesh;

    async run() {
        // 1. Register and install extensions (BEFORE getPage)
        this.mesh.service(BrowserExtensionsService);
        await this.browserExtensionsService.installExtensions(
            true,
            this.browserService.userDataDir,
        );

        // 2. Now get the page — extensions will be active
        const page = await this.browserService.getPage();
        await page.goto('https://example.com');

        // ... rest of your workflow
    }
}
```

**Key points:**
- `this.mesh.service(BrowserExtensionsService)` registers the service in the DI container
- `installExtensions(true, ...)` — first argument is `isPersistent` (matches against `loadInPersistent`/`loadInTransient` in config)
- Extensions are installed into the Chrome user data directory, so they persist across runs
- The service uses Puppeteer internally to enable Chrome developer mode and load unpacked extensions
- Extensions only get installed once — subsequent runs detect they are already present and skip installation

## Project Structure

```
├── chrome-extensions/           # Browser extensions (see Chrome Extensions)
│   ├── extensions.json          # Extension manifest
│   └── idcac/                   # Cookie consent handler (pre-installed)
├── datasets/                    # Input YAML files (one per service)
├── services/                    # Output directory (gitignored)
│   └── {service-id}/
│       ├── screenshot.png       # Page screenshot
│       └── result.json          # Workflow result
├── src/
│   ├── instructions/            # LLM instructions (markdown files)
│   ├── schema/                  # Zod schemas (Dataset, Result)
│   └── workflows/               # Workflow classes
```

## Included Workflows

### SimpleScreenshot

A minimal workflow demonstrating basic browser automation:
1. Navigates to a URL
2. Waits for page load
3. Takes a full-page screenshot

Useful as a starting point or for testing browser connectivity.

### SimpleGenericWebAgent

Demonstrates the `GenericWebAgent` pattern:
1. Navigates to a URL
2. Creates a `GenericWebAgent` via `AgentsService`
3. Runs the agent with a simple text objective

This is the simplest way to use LLM-powered web automation - no stages, no schemas, just a one-shot LLM call with browser tools.

### SimpleLearningAgentExtract

Demonstrates the learning agent pattern with stages:
1. Loads instructions from a markdown file
2. Sets up automation context with input/output schemas
3. Uses `AutomationRunner.learn()` to execute with LLM guidance
4. The LLM creates reusable stages to navigate and extract data

Use this pattern for complex, multi-step automations that benefit from reusable stages.

## Getting Started

1. Set up environment variables (see [Environment Setup](#environment-setup))
2. Run `npm run compile` to build the project
3. Update the dataset in the workflow (or load from `datasets/`)
4. Define instructions in `src/instructions/` for LLM-powered workflows
5. Update schemas in `src/schema/` to match your data

## Key Services

- `ChatService` — Send messages to the UI
- `BrowserService` — Access the browser page for screenshots, navigation
- `AgentsService` — Create LLM agents (`createGenericWebAgent()`, `createPageExtractor()`)
- `AutomationRunner` — Run automations with `run()` (existing stages) or `learn()` (LLM-guided)
- `AutomationContext` — Manage automation state, inputs, outputs, and schemas

## GenericWebAgent Pattern

For simple one-shot LLM tasks, use `GenericWebAgent`:

```typescript
import { AgentsService, BrowserService, workflow } from '@athree/runner';
import { dep } from 'mesh-ioc';

@workflow({ title: 'My Workflow' })
export class MyWorkflow {
    @dep() private browserService!: BrowserService;
    @dep() private agentsService!: AgentsService;

    async run() {
        const page = await this.browserService.getPage();
        await page.goto('https://example.com');

        const agent = this.agentsService.createGenericWebAgent();
        agent.addUserContext({ currentUrl: page.url() });

        await agent.run('Extract the main heading and description from this page.');
    }
}
```

## Learning Agent Pattern

For complex multi-step automations with reusable stages, use the learning agent:

```typescript
import { AutomationRunner, AutomationContext, workflow } from '@athree/runner';
import { dep } from 'mesh-ioc';
import { z } from 'zod';

@workflow({ title: 'My Workflow' })
export class MyWorkflow {
    @dep() private automationRunner!: AutomationRunner;
    @dep() private automation!: AutomationContext;

    async run() {
        const result = await this.automationRunner.learn({
            automationId: 'my-automation',
            instructions,           // Loaded from .md file
            inputs: { url },        // Input data
            inputsSchema,           // Zod schema for inputs
            outputsSchema,          // Zod schema for expected outputs
            learningModel: 'gemini-2.5-flash',  // Optional: specify LLM model
        });

        const outputs = this.automation.outputs;
    }
}
```

## Instructions Files

Instructions tell the LLM how to handle different page states. Place them in `src/instructions/`:

```markdown
# Goal
Extract product details from the page.

# Output Fields
- `ctx.outputs.title` - Product title
- `ctx.outputs.price` - Product price
- `ctx.outputs.status` - "success" or "failure"

# Instructions
## Product page
1. Extract the title from h1
2. Extract the price
3. Set status to "success"
```

## Tips

- Define clear output fields in your instructions
- Use Zod schemas to validate inputs and outputs
- The LLM creates reusable stages that can be re-run without LLM
- Save results to the `services/` folder
