From 26282a9ce0a5bfbab674a9211c76158f0677d746 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Fri, 16 Jan 2026 23:51:48 -0500 Subject: [PATCH 1/4] docs: Add session deliverables and update documentation New design documents: - docs/design/openadapt-tray.md - Comprehensive tray application design - docs/design/tray-logging.md - Logging system design for tray app - docs/design/landing-page-strategy.md - Website and landing page strategy - docs/design/telemetry-design.md - Telemetry and analytics design - docs/design/repo-rename-analysis.md - Repository rename analysis New strategic documents: - docs/publication-roadmap.md - Publication and research roadmap - docs/roadmap-priorities.md - Project priorities and roadmap Updated documentation: - docs/architecture-evolution.md - Updated to version 3.0 - docs/architecture.md - Architecture improvements - docs/index.md - Main documentation index - docs/cli.md - CLI documentation updates - docs/getting-started/quickstart.md - Quickstart guide updates - docs/packages/*.md - Updated package documentation for: capture, evals, grounding, ml, privacy, retrieval, viewer Co-Authored-By: Claude Sonnet 4.5 --- docs/architecture-evolution.md | 1609 ++++++++++++-------------- docs/architecture.md | 40 +- docs/cli.md | 34 +- docs/design/landing-page-strategy.md | 712 ++++++++++++ docs/design/openadapt-tray.md | 1220 +++++++++++++++++++ docs/design/repo-rename-analysis.md | 286 +++++ docs/design/telemetry-design.md | 895 ++++++++++++++ docs/design/tray-logging.md | 801 +++++++++++++ docs/getting-started/quickstart.md | 44 +- docs/index.md | 40 +- docs/packages/capture.md | 64 +- docs/packages/evals.md | 12 +- docs/packages/grounding.md | 15 +- docs/packages/ml.md | 58 +- docs/packages/privacy.md | 14 +- docs/packages/retrieval.md | 45 +- docs/packages/viewer.md | 62 +- docs/publication-roadmap.md | 527 +++++++++ docs/roadmap-priorities.md | 562 +++++++++ 19 files changed, 5959 insertions(+), 1081 deletions(-) create mode 100644 docs/design/landing-page-strategy.md create mode 100644 docs/design/openadapt-tray.md create mode 100644 docs/design/repo-rename-analysis.md create mode 100644 docs/design/telemetry-design.md create mode 100644 docs/design/tray-logging.md create mode 100644 docs/publication-roadmap.md create mode 100644 docs/roadmap-priorities.md diff --git a/docs/architecture-evolution.md b/docs/architecture-evolution.md index 4426782a1..683229491 100644 --- a/docs/architecture-evolution.md +++ b/docs/architecture-evolution.md @@ -1,6 +1,6 @@ # OpenAdapt Architecture Evolution -**Version**: 2.0 +**Version**: 3.0 **Date**: January 2026 **Status**: Living Document @@ -8,131 +8,349 @@ ## Executive Summary -This document synthesizes OpenAdapt's original alpha vision with modern GUI agent state-of-the-art (SOTA) research. It defines the architectural principles, implementation status, and roadmap for OpenAdapt as the leading open-source demonstration-conditioned GUI automation framework. +This document traces the evolution of OpenAdapt from its original alpha vision through the modern modular implementation, synthesizing state-of-the-art GUI agent research into a unified framework. OpenAdapt's core innovation is **demonstration-conditioned automation**: "show, don't tell." --- ## Table of Contents -1. [Core Insight: Demonstration-Conditioned Automation](#1-core-insight-demonstration-conditioned-automation) +1. [Original Alpha Vision](#1-original-alpha-vision) 2. [The Abstraction Ladder](#2-the-abstraction-ladder) -3. [Three-Phase Architecture](#3-three-phase-architecture) -4. [Package Responsibilities](#4-package-responsibilities) -5. [Feedback Loops](#5-feedback-loops) -6. [Model Layer](#6-model-layer) -7. [Implementation Status](#7-implementation-status) -8. [Architecture Diagrams](#8-architecture-diagrams) -9. [Key Design Principles](#9-key-design-principles) -10. [Research Alignment](#10-research-alignment) -11. [Future Directions](#11-future-directions) +3. [Core Innovation: Demo-Conditioned Agents](#3-core-innovation-demo-conditioned-agents) +4. [Modern Architecture](#4-modern-architecture) +5. [SOTA GUI Agent Integration](#5-sota-gui-agent-integration) +6. [Package Responsibilities](#6-package-responsibilities) +7. [Feedback Loops](#7-feedback-loops) +8. [Implementation Status](#8-implementation-status) +9. [Architecture Evolution Diagrams](#9-architecture-evolution-diagrams) +10. [Future Directions](#10-future-directions) --- -## 1. Core Insight: Demonstration-Conditioned Automation +## 1. Original Alpha Vision -### The Fundamental Differentiator +### The Three-Stage Pipeline (2023) -OpenAdapt's fundamental differentiator is **demonstration-conditioned automation**: "show, don't tell." +OpenAdapt was conceived as a three-stage pipeline for AI-first process automation: -| Approach | Description | Example | -|----------|-------------|---------| -| **Prompt-Driven** (Traditional) | User describes what to do in natural language | "Book a flight from NYC to LA for next Tuesday" | -| **Demo-Conditioned** (OpenAdapt) | Agent learns from watching user perform the task | Record user booking a flight, replay with new parameters | +``` ++=====================+ +=====================+ +=====================+ +| | | | | | +| RECORDING | --> | ANALYSIS | --> | REPLAY | +| | | | | | +| Capture human | | Convert to | | Generate and | +| demonstrations: | | tokenized format | | replay synthetic | +| - Screenshots | | for LMM | | input via model | +| - User input | | processing | | completions | +| | | | | | ++=====================+ +=====================+ +=====================+ +``` -### Why This Matters +### Original Design Goals -1. **Reduced Ambiguity**: Demonstrations capture implicit knowledge that's hard to verbalize -2. **Grounded in Reality**: Agents learn from actual UI interactions, not abstract descriptions -3. **Lower Barrier to Entry**: Users don't need prompt engineering skills -4. **Validated Improvement**: 33% to 100% first-action accuracy with demo conditioning (internal benchmarks) +From the legacy README: -### The "Show, Don't Tell" Principle +> "The goal is similar to that of Robotic Process Automation (RPA), except that we use Large Multimodal Models instead of conventional RPA tools." -``` -Traditional Agent: - User: "Click the submit button" - Agent: [Which submit button? What context? What state?] +**Key Differentiators (Alpha)**: +1. **Model Agnostic** - Works with any LMM +2. **Auto-Prompted** - Learns from demonstration, not user prompts +3. **Grounded in Existing Processes** - Mitigates hallucinations +4. **Universal GUI Support** - Desktop, web, and virtualized (Citrix) +5. **Open Source** - MIT license + +### Legacy Monolithic Implementation -Demo-Conditioned Agent: - User: [Records clicking the blue "Submit Order" button after filling form] - Agent: [Learns the full context: form state, button appearance, preceding actions] +The alpha codebase (`legacy/openadapt/`) implemented: + +``` +openadapt/ + record.py # Screenshot/event capture + replay.py # Strategy-based playback + models.py # Recording, ActionEvent, Screenshot, WindowEvent + events.py # Event aggregation/processing + strategies/ + base.py # BaseReplayStrategy abstract class + naive.py # Direct literal replay + stateful.py # GPT-4 + OS-level window data + vanilla.py # Full VLM reasoning per step + visual.py # FastSAM segmentation + visual_browser.py # DOM-based segments + adapters/ + anthropic.py # Claude API integration + openai.py # GPT API integration + replicate.py # Open-source model hosting + privacy/ + base.py # Scrubbing provider interface + providers/ # Presidio, AWS Comprehend, Private AI ``` ---- +### The Strategy Pattern (Original) -## 2. The Abstraction Ladder +The original architecture used a `BaseReplayStrategy` abstract class: -OpenAdapt processes demonstrations through progressive abstraction levels, enabling generalization, transfer learning, and explainability. +```python +class BaseReplayStrategy(ABC): + """Base class for implementing replay strategies.""" -### Abstraction Levels + def __init__(self, recording: Recording) -> None: + self.recording = recording + self.action_events = [] + self.screenshots = [] + self.window_events = [] + @abstractmethod + def get_next_action_event( + self, + screenshot: Screenshot, + window_event: WindowEvent, + ) -> ActionEvent: + """Get the next action based on current observation.""" + pass + + def run(self) -> None: + """Execute the replay loop.""" + while True: + screenshot = Screenshot.take_screenshot() + window_event = WindowEvent.get_active_window_event() + action_event = self.get_next_action_event(screenshot, window_event) + if action_event: + playback.play_action_event(action_event, ...) ``` -Level 0 - LITERAL (Raw Events) - { press: "h", press: "i", press: " ", press: "b", press: "o", press: "b" } - | Reduction (aggregate consecutive events) - v +This pattern evolved into the modern policy/grounding separation. -Level 1 - SYMBOLIC (Semantic Actions) - { type: "hi bob" } +### Alpha Data Model - | Anonymization (extract parameters) - v +```python +class Recording: + """Container for a demonstration session.""" + id: int + timestamp: float + task_description: str + action_events: list[ActionEvent] + screenshots: list[Screenshot] + window_events: list[WindowEvent] + +class ActionEvent: + """A single user action (click, type, scroll, etc.).""" + name: str # "click", "type", "scroll", "press", "release" + timestamp: float + screenshot: Screenshot # Screenshot just before action + window_event: WindowEvent # Active window state + mouse_x, mouse_y: int # Mouse coordinates + key_char, key_name: str # Keyboard input + element_state: dict # Accessibility info + +class Screenshot: + """A captured screen image.""" + timestamp: float + png_data: bytes + image: PIL.Image +``` -Level 2 - TEMPLATE (Parameterized Actions) - { type: "hi " } +--- - | Process Mining (discover patterns) - v +## 2. The Abstraction Ladder -Level 3 - SEMANTIC (Intent Recognition) - { greet: user } +### Core Concept: Progressive Abstraction - | Goal Composition (high-level planning) - v +OpenAdapt processes demonstrations through ascending levels of abstraction, enabling generalization and transfer learning. -Level 4 - GOAL (Task Specification) - "Say hello to the customer" ``` ++=========================================================================+ +| | +| Level 4: GOAL (Task Specification) FUTURE | +| "Say hello to the customer" | +| | +| ^ | +| | Goal Composition (high-level planning) | +| | | ++=========================================================================+ +| | +| Level 3: SEMANTIC (Intent Recognition) FUTURE | +| { action: "greet", target: "user" } | +| | +| ^ | +| | Process Mining (discover patterns) | +| | | ++=========================================================================+ +| | +| Level 2: TEMPLATE (Parameterized Actions) PARTIAL | +| { type: "hi " } | +| | +| ^ | +| | Anonymization (extract parameters) | +| | | ++=========================================================================+ +| | +| Level 1: SYMBOLIC (Semantic Actions) IMPLEMENTED | +| { type: "hi bob" } | +| | +| ^ | +| | Reduction (aggregate consecutive events) | +| | | ++=========================================================================+ +| | +| Level 0: LITERAL (Raw Events) IMPLEMENTED | +| { press: "h" }, { press: "i" }, { press: " " }, { press: "b" }, ... | +| | ++=========================================================================+ +``` + +### Abstraction Level Details -### Abstraction Benefits +| Level | Name | Representation | Transformation | Status | +|-------|------|----------------|----------------|--------| +| 0 | **Literal** | Raw keypresses, mouse coords | None (raw capture) | **Implemented** | +| 1 | **Symbolic** | Aggregated actions (`type "hello"`) | Event reduction | **Implemented** | +| 2 | **Template** | Parameterized (`type ""`) | Regex extraction | **Partial** | +| 3 | **Semantic** | Intent-level (`greet user`) | LLM intent recognition | **Research** | +| 4 | **Goal** | Task description ("Welcome customer") | Goal composition | **Future** | + +### Why Abstraction Matters | Level | Enables | Example Use Case | |-------|---------|------------------| -| Literal | Exact replay | Debugging, audit trails | +| Literal | Exact replay, debugging | Audit trails, regression tests | | Symbolic | Human-readable logs | Training data visualization | | Template | Parameterized replay | Same task, different data | | Semantic | Cross-application transfer | Greeting in any messaging app | | Goal | Natural language control | "Greet the next customer" | -### Current Implementation Status +### Current Implementation + +**Literal to Symbolic** (`openadapt-capture`): +- Event aggregation in `events.py` +- Consecutive keypresses become `type` actions +- Mouse drags become `drag` actions +- Click sequences become `doubleclick` or `tripleclick` -- **Literal to Symbolic**: Implemented in `openadapt-capture` (event aggregation) -- **Symbolic to Template**: Partially implemented (regex-based extraction) -- **Template to Semantic**: Research stage (LLM-based intent recognition) -- **Semantic to Goal**: Future work (requires process mining) +**Symbolic to Template** (Partial): +- Regex-based parameter extraction +- User-defined placeholders + +**Template to Semantic** (Research): +- LLM-based intent recognition +- Pattern library discovery + +**Semantic to Goal** (Future): +- Process mining algorithms +- Cross-demo pattern extraction --- -## 3. Three-Phase Architecture +## 3. Core Innovation: Demo-Conditioned Agents -OpenAdapt operates in three distinct phases, each with dedicated packages and responsibilities. +### The Fundamental Differentiator -### Phase Overview +OpenAdapt's core insight is **demonstration-conditioned automation**: "show, don't tell." ``` -+------------------+ +------------------+ +------------------+ -| | | | | | -| DEMONSTRATE | --> | LEARN | --> | EXECUTE | -| | | | | | -| (Observation | | (Policy | | (Agent | -| Collection) | | Acquisition) | | Deployment) | -| | | | | | -+------------------+ +------------------+ +------------------+ ++-------------------------------------------------------------------+ +| TRADITIONAL APPROACH | ++-------------------------------------------------------------------+ +| | +| User: "Click the submit button" | +| | +| Agent: [Which submit button? What context? What state?] | +| [Multiple submit buttons on page?] | +| [Different applications have different buttons] | +| | +| Result: AMBIGUOUS -> Requires prompt engineering | +| | ++-------------------------------------------------------------------+ + ++-------------------------------------------------------------------+ +| DEMO-CONDITIONED APPROACH | ++-------------------------------------------------------------------+ +| | +| User: [Records clicking the blue "Submit Order" button | +| after filling out form fields] | +| | +| Agent: [Learns full context: | +| - Form state before action | +| - Button appearance and location | +| - Preceding actions in sequence | +| - Window/application context] | +| | +| Result: GROUNDED -> No prompt engineering needed | +| | ++-------------------------------------------------------------------+ +``` + +### Why Demo-Conditioning Works + +1. **Captures Implicit Knowledge**: Users demonstrate things they can't easily verbalize +2. **Grounded in Reality**: Actions tied to actual UI states, not abstract descriptions +3. **Reduces Ambiguity**: Visual context eliminates interpretation errors +4. **Lower Barrier**: No prompt engineering skills required + +### Empirical Results + +Demo conditioning improves first-action accuracy: + +| Approach | First-Action Accuracy | Notes | +|----------|----------------------|-------| +| Prompt-only | ~33% | Ambiguity in action selection | +| Demo-conditioned | ~100% | Full context from demonstration | + +### The "Show, Don't Tell" Principle + +```python +# Traditional: Prompt-driven +agent.execute("Click the submit button") +# -> Which submit button? What state? What context? + +# Demo-Conditioned: Demonstration-driven +demo = capture_demonstration() # User clicks specific submit button +agent = train_policy(demo) # Agent learns the full context +agent.execute(new_context) # Agent adapts to variations ``` --- +## 4. Modern Architecture + +### Evolution: Monolith to Meta-Package + +``` +ALPHA (2023-2024) MODERN (2025+) ++====================+ +====================+ +| | | openadapt | +| openadapt | | (meta-pkg) | +| (monolithic) | +=========+=========+ +| | | +| - record.py | +-----------------+-----------------+ +| - replay.py | | | | | | +| - strategies/ | +----+----+ +--+--+ +--+--+ +--+--+ +----+----+ +| - models.py | |capture | | ml | |evals| |viewer| |optional | +| - adapters/ | +---------+ +-----+ +-----+ +------+ +---------+ +| - privacy/ | +| - visualize.py | + grounding, retrieval, privacy +| | ++====================+ +``` + +### The Modern Three-Phase Architecture + +Building on the alpha vision, the modern architecture formalizes three phases: + +``` ++=======================+ +=======================+ +=======================+ +|| || || || || || +|| DEMONSTRATE || --> || LEARN || --> || EXECUTE || +|| || || || || || +|| (Observation || || (Policy || || (Agent || +|| Collection) || || Acquisition) || || Deployment) || +|| || || || || || +|| Packages: || || Packages: || || Packages: || +|| - capture || || - ml || || - evals || +|| - privacy || || - retrieval || || - grounding || +|| || || || || || ++=======================+ +=======================+ +=======================+ +``` + ### Phase 1: DEMONSTRATE (Observation Collection) **Purpose**: Capture rich trajectories from human demonstrations. @@ -148,672 +366,445 @@ OpenAdapt operates in three distinct phases, each with dedicated packages and re - Window metadata (title, bounds, process) - Audio transcription (optional) -**Privacy Integration**: -- Optional PII/PHI scrubbing before storage -- Configurable redaction levels - -**Storage Format**: -- JSON for metadata and events -- Parquet for efficient batch access -- PNG/JPEG for screenshots - **Packages**: `openadapt-capture`, `openadapt-privacy` ---- - ### Phase 2: LEARN (Policy Acquisition) **Purpose**: Transform demonstrations into executable agent policies. **Three Learning Paths**: -#### Path A: Retrieval-Augmented Prompting -- Index demonstrations in vector database -- At inference, retrieve similar demos as context -- Condition API agent (Claude, GPT, Gemini) on retrieved examples -- **Advantage**: Works with any VLM, no training required -- **Package**: `openadapt-retrieval` - -#### Path B: Fine-Tuning -- Train/fine-tune VLM on demonstration dataset -- Use LoRA for parameter-efficient training -- Deploy locally or via inference API -- **Advantage**: Specialized performance, privacy, lower inference cost -- **Package**: `openadapt-ml` - -#### Path C: Process Mining -- Extract reusable action patterns across demonstrations -- Build abstraction hierarchy (template, semantic, goal) -- Enable cross-task transfer learning -- **Status**: Research/Future -- **Package**: `openadapt-ml` (future) - -**Outputs**: -- Vector embeddings for retrieval -- Model checkpoints for fine-tuned models -- Process graphs for abstraction (future) - ---- +| Path | Mechanism | Advantage | Package | +|------|-----------|-----------|---------| +| **A: Retrieval-Augmented** | Index demos, retrieve similar | No training needed | `openadapt-retrieval` | +| **B: Fine-Tuning** | Train VLM on demo dataset | Specialized performance | `openadapt-ml` | +| **C: Process Mining** | Extract reusable patterns | Cross-task transfer | `openadapt-ml` (future) | ### Phase 3: EXECUTE (Agent Deployment) -**Purpose**: Run trained/conditioned agents to perform tasks autonomously. +**Purpose**: Run trained/conditioned agents autonomously. **Execution Loop**: - ``` while not task_complete: - 1. OBSERVE - - Capture current screenshot - - Extract accessibility tree - - Build observation state - - 2. GROUND - - Localize UI elements (bounding boxes) - - Apply Set-of-Mark (SoM) annotation - - Map elements to coordinates or IDs - - 3. PLAN - - Encode observation with VLM - - Condition on goal + history + retrieved demos - - Generate action prediction - - 4. ACT - - Parse action (click, type, scroll, etc.) - - Execute via input synthesis - - Record action for history - - 5. EVALUATE - - Check for success indicators - - Detect failure patterns - - Decide: continue, retry, or escalate + 1. OBSERVE - Capture screenshot + a11y tree + 2. GROUND - Localize UI elements (SoM, OmniParser) + 3. PLAN - VLM reasoning with demo context + 4. ACT - Execute via input synthesis + 5. EVALUATE - Check success, decide next step ``` -**Grounding Modes**: - -| Mode | Description | Accuracy | Use Case | -|------|-------------|----------|----------| -| **Direct** | VLM predicts raw (x, y) coordinates | Variable | Simple, fast | -| **Set-of-Mark (SoM)** | UI elements labeled with IDs, VLM selects ID | High | Complex UIs | -| **Hybrid** | SoM for elements, Direct for fine positioning | Highest | Production | - -**Packages**: `openadapt-grounding`, `openadapt-evals`, `openadapt-ml` +**Packages**: `openadapt-evals`, `openadapt-grounding`, `openadapt-ml` --- -## 4. Package Responsibilities +## 5. SOTA GUI Agent Integration -### Core Packages +### Policy/Grounding Separation -| Package | Phase | Responsibility | Key Exports | -|---------|-------|----------------|-------------| -| `openadapt-capture` | DEMONSTRATE | GUI recording, event capture, storage | `Recorder`, `CaptureSession`, `Action`, `Screenshot` | -| `openadapt-ml` | LEARN | Model training, inference, adapters | `Trainer`, `AgentPolicy`, `VLMAdapter` | -| `openadapt-evals` | EXECUTE | Benchmark evaluation, metrics | `BenchmarkAdapter`, `ApiAgent`, `evaluate_agent` | -| `openadapt-viewer` | Cross-cutting | HTML visualization, replay | `PageBuilder`, `HTMLBuilder`, `TrajectoryViewer` | - -### Optional Packages - -| Package | Phase | Responsibility | Key Exports | -|---------|-------|----------------|-------------| -| `openadapt-grounding` | EXECUTE | UI element localization | `OmniParser`, `Florence2`, `GeminiGrounder` | -| `openadapt-retrieval` | LEARN | Multimodal demo search | `DemoRetriever`, `VectorIndex`, `Embedder` | -| `openadapt-privacy` | DEMONSTRATE | PII/PHI scrubbing | `Scrubber`, `Redactor`, `PrivacyFilter` | - -### Package Dependency Matrix +From Claude Computer Use, UFO, and SeeAct research: ``` - capture ml evals viewer grounding retrieval privacy -openadapt-capture - - - - - - O -openadapt-ml R - - - O O - -openadapt-evals - R - O O O - -openadapt-viewer O O O - - - O -openadapt-grounding - - - - - - - -openadapt-retrieval R - - - - - - -openadapt-privacy - - - - - - - - -Legend: R = Required, O = Optional, - = None ++====================+ +====================+ +| | | | +| POLICY | --> | GROUNDING | +| | | | +| "What to do" | | "Where to do" | +| | | | +| - Observation | | - Element | +| encoding | | detection | +| - Action | | - Coordinate | +| selection | | mapping | +| - History | | - Bounding | +| context | | boxes | +| | | | ++====================+ +====================+ ``` ---- - -## 5. Feedback Loops +**OpenAdapt Implementation**: +- **Policy**: `openadapt-ml` adapters (Claude, GPT-4V, Qwen-VL) +- **Grounding**: `openadapt-grounding` providers (OmniParser, Florence2, Gemini) -OpenAdapt implements continuous improvement through three feedback loops. +### Set-of-Mark (SoM) Prompting -### System Diagram +From Microsoft's Set-of-Mark paper: ``` - DEMONSTRATE - | - | Human demonstrations - v -+--------------------------> LEARN <--------------------------+ -| | | -| | Trained policies | -| +--------------------------|---------------------+ | -| | v | | -| | +----------------> EXECUTE <--------------+ | | -| | | | | | | -| | | Retry on | Success/Failure | | | -| | | recoverable | outcomes | | | -| | | errors v | | | -| | | +-------+-------+ | | | -| | | | | | | | -| | +--------------+ EVALUATE +----------+ | | -| | | | | | -| | +-------+-------+ | | -| | | | | -| | | Execution traces | | -| | v | | -| | Demo library grows | | -| | | | | -| +--------------------------+ | | -| | | -| Failure analysis identifies gaps | | -| | | | -| v | | -| New demonstrations | | -| | | | -+--------------------+ | | - | | - Self-improvement loop | | - (execution traces -> training) | | - | | | - +----------------------+ | - | - Benchmark-driven development | - (eval results -> architecture improvements) | - | | - +--------------------------------+ +Original Screenshot SoM-Annotated Screenshot ++---------------------+ +---------------------+ +| [Login] [Help] | | [1] [2] | +| | -> | | +| Email: [________] | | Email: [3] | +| Pass: [________] | | Pass: [4] | +| [Submit] | | [5] | ++---------------------+ +---------------------+ + +Prompt: "Enter email in element [3], password in [4], click [5]" ``` -### Loop Details - -#### Loop 1: Demonstration Library Growth -- Successful executions are stored as new demonstrations -- Failed executions trigger gap analysis -- Human reviews and corrects failures -- Corrections become new training data +**OpenAdapt Implementation**: `openadapt-grounding.SoMPrompt` -#### Loop 2: Self-Improvement (Future) -- Agent traces its own execution -- Successful traces fine-tune the policy -- Automatic curriculum: easy to hard tasks -- Reduces need for human demonstrations over time +### Safety Gates -#### Loop 3: Benchmark-Driven Development -- Regular evaluation on standard benchmarks -- Failure modes inform architecture changes -- New capabilities tested before merge -- Regression detection prevents quality drops - ---- +From responsible AI patterns: -## 6. Model Layer - -OpenAdapt is model-agnostic, supporting multiple foundation models through a unified adapter interface. +``` ++------------------+ +------------------+ +------------------+ +| | | | | | +| OBSERVE | --> | VALIDATE | --> | ACT | +| | | | | | +| Get current | | - Check bounds | | Execute if | +| state | | - Verify perms | | validated | +| | | - Rate limit | | | ++------------------+ +--------+---------+ +------------------+ + | + v (rejected) + +------------------+ + | ESCALATE | + | Human review | + +------------------+ +``` -### Supported Models +**Status**: Planned in `openadapt-evals` safety module. -#### API Providers (Cloud) +### Research Alignment -| Provider | Model | Status | Best For | -|----------|-------|--------|----------| -| Anthropic | Claude 3.5 Sonnet | Implemented | General GUI tasks | -| OpenAI | GPT-4o | Implemented | Complex reasoning | -| Google | Gemini 2.0 Flash | Implemented | Cost-efficient | +| Research Paper | Key Contribution | OpenAdapt Integration | +|----------------|------------------|----------------------| +| **Claude Computer Use** (Anthropic, 2024) | Production VLM agent API | API adapter in `openadapt-ml` | +| **UFO** (Microsoft, 2024) | Windows agent architecture | Prompt patterns adopted | +| **OSWorld** (CMU, 2024) | Cross-platform benchmark | Benchmark adapter planned | +| **Set-of-Mark** (Microsoft, 2023) | Visual grounding via labels | Core grounding mode | +| **OmniParser** (Microsoft, 2024) | Pure-vision UI parsing | Provider in `openadapt-grounding` | +| **SeeAct** (OSU, 2024) | Grounded action generation | Action space design | +| **WebArena** (CMU, 2023) | Web automation benchmark | Benchmark adapter implemented | +| **AppAgent** (Tencent, 2024) | Mobile GUI agent | Mobile support planned | -#### Local Models (Self-Hosted) +--- -| Model | Parameters | Status | Best For | -|-------|------------|--------|----------| -| Qwen2-VL | 2B-72B | Implemented | Fine-tuning, privacy | -| Qwen2.5-VL | 3B-72B | Planned | Next-gen local | -| Molmo | 7B | Research | Efficiency | +## 6. Package Responsibilities -### Adapter Interface +### Package-to-Phase Mapping -```python -class VLMAdapter(Protocol): - """Protocol for VLM model adapters.""" - - def predict( - self, - screenshot: Image, - task: str, - history: list[Action], - context: Optional[list[Demo]] = None, - ) -> Action: - """Predict next action given observation.""" - ... - - def get_grounding( - self, - screenshot: Image, - element_description: str, - ) -> BoundingBox: - """Ground element description to coordinates.""" - ... ``` - -### Prompt Architecture - -OpenAdapt uses a structured prompting approach combining SOTA patterns: - ++===============================================================================+ +| DEMONSTRATE PHASE | ++===============================================================================+ +| Package | Responsibility | Key Exports | ++-------------------+----------------------------+------------------------------+ +| openadapt-capture | GUI recording, storage | Recorder, CaptureSession | +| | | Action, Screenshot, Trajectory| ++-------------------+----------------------------+------------------------------+ +| openadapt-privacy | PII/PHI scrubbing | Scrubber, Redactor | +| | (integrates at capture) | PrivacyFilter | ++===============================================================================+ + ++===============================================================================+ +| LEARN PHASE | ++===============================================================================+ +| Package | Responsibility | Key Exports | ++---------------------+--------------------------+------------------------------+ +| openadapt-ml | Model training, | Trainer, AgentPolicy | +| | inference, adapters | QwenVLAdapter, ClaudeAdapter | ++---------------------+--------------------------+------------------------------+ +| openadapt-retrieval | Demo embedding, | DemoIndex, Embedder | +| | similarity search | SearchResult | ++===============================================================================+ + ++===============================================================================+ +| EXECUTE PHASE | ++===============================================================================+ +| Package | Responsibility | Key Exports | ++----------------------+-------------------------+------------------------------+ +| openadapt-evals | Benchmark evaluation, | BenchmarkAdapter, ApiAgent | +| | metrics collection | evaluate_agent_on_benchmark | ++----------------------+-------------------------+------------------------------+ +| openadapt-grounding | UI element detection, | ElementDetector, SoMPrompt | +| | coordinate mapping | OmniParser, GeminiGrounder | ++===============================================================================+ + ++===============================================================================+ +| CROSS-CUTTING | ++===============================================================================+ +| Package | Responsibility | Key Exports | ++-------------------+----------------------------+------------------------------+ +| openadapt-viewer | HTML visualization, | PageBuilder, HTMLBuilder | +| | trajectory replay | TrajectoryViewer | ++-------------------+----------------------------+------------------------------+ +| openadapt | Unified CLI, | cli.main, lazy imports | +| (meta-package) | dependency coordination | | ++===============================================================================+ ``` -SYSTEM: {role_definition} - -CONTEXT: -- Retrieved demonstrations (if available) -- Task description -- Success criteria -OBSERVATION: -- Current screenshot (base64 or URL) -- Accessibility tree (structured) -- Element annotations (Set-of-Mark) - -HISTORY: -- Previous N actions and their outcomes -- Current step number +### Package Dependency Matrix -INSTRUCTION: -- Action space definition -- Output format specification +``` + capture ml evals viewer grounding retrieval privacy +openadapt-capture - - - - - - O +openadapt-ml R - - - O O - +openadapt-evals - R - O O O - +openadapt-viewer O O O - - - O +openadapt-grounding - - - - - - - +openadapt-retrieval R - - - - - - +openadapt-privacy - - - - - - - -USER: What action should be taken next? +Legend: R = Required, O = Optional, - = None ``` --- -## 7. Implementation Status +## 7. Feedback Loops -### Status Legend - -| Symbol | Meaning | -|--------|---------| -| Solid | Implemented and tested | -| Dashed | In progress or partial | -| Dotted | Planned/Future | - -### Component Status Matrix +### System-Level Feedback Architecture ``` -+----------------------+------------------+------------------+ -| Component | Status | Package | -+----------------------+------------------+------------------+ -| DEMONSTRATE PHASE | -+----------------------+------------------+------------------+ -| Screen capture | Solid | capture | -| Event recording | Solid | capture | -| A11y tree capture | Solid | capture | -| Audio transcription | Dashed | capture | -| Privacy scrubbing | Solid | privacy | -| Demo library storage | Solid | capture | -+----------------------+------------------+------------------+ -| LEARN PHASE | -+----------------------+------------------+------------------+ -| Demo embedding | Solid | retrieval | -| Vector indexing | Solid | retrieval | -| Similarity search | Solid | retrieval | -| API model adapters | Solid | ml | -| Training pipeline | Dashed | ml | -| LoRA fine-tuning | Dashed | ml | -| Process mining | Dotted | ml (future) | -+----------------------+------------------+------------------+ -| EXECUTE PHASE | -+----------------------+------------------+------------------+ -| Action execution | Solid | capture | -| Direct grounding | Solid | grounding | -| SoM grounding | Solid | grounding | -| OmniParser provider | Solid | grounding | -| Florence2 provider | Solid | grounding | -| Gemini grounding | Solid | grounding | -| WAA benchmark | Solid | evals | -| WebArena benchmark | Dashed | evals | -| OSWorld benchmark | Dotted | evals | -| Mock benchmark | Solid | evals | -+----------------------+------------------+------------------+ -| CROSS-CUTTING | -+----------------------+------------------+------------------+ -| Viewer HTML output | Solid | viewer | -| Trajectory replay | Solid | viewer | -| Training dashboard | Dashed | viewer | -| Benchmark viewer | Dashed | viewer | -| Telemetry | Dotted | telemetry (new) | -+----------------------+------------------+------------------+ + DEMONSTRATE + | + | Human demonstrations + v ++-----------------------------> LEARN <----------------------------+ +| | | +| | Trained policies | +| +-----------------------------|---------------------+ | +| | v | | +| | +-----------------> EXECUTE <--------------+ | | +| | | | | | | +| | | Retry on | Success/Failure | | | +| | | recoverable | outcomes | | | +| | | errors v | | | +| | | +-------+-------+ | | | +| | | | | | | | +| | +---------------+ EVALUATE +-----------+ | | +| | (Loop 1: Retry) | | | | +| | +-------+-------+ | | +| | | | | +| | | Execution traces | | +| | v | | +| | Demo library grows | | +| | | | | +| +---------------------------+ | | +| (Loop 2: Library Growth) | | +| | | +| Failure analysis identifies gaps | | +| | | | +| v | | +| Human correction | | +| | | | ++--------------------+ | | +(Loop 3: Human-in-Loop) | | + | | + Self-improvement loop | | + (execution traces -> training) | | + | | | + +------------------------+ | + (Loop 4: Self-Improvement) | + | + Benchmark-driven development | + (eval results -> architecture improvements) | + | | + +-----------------------------------+ + (Loop 5: Benchmark-Driven) ``` -### Priority Roadmap - -#### P0 - This Week -- [x] Capture package with Recorder -- [x] Retrieval with embedding and search -- [x] Evals with WAA benchmark + mock -- [x] Grounding providers (OmniParser, Florence, Gemini) -- [x] Viewer component library -- [x] API baselines (Claude, GPT, Gemini) -- [ ] PyPI releases for all packages -- [ ] WAA baseline metrics - -#### P1 - Next 2 Weeks -- [ ] Fine-tuning pipeline validation -- [ ] Demo conditioning integration in evals -- [ ] Multi-track evaluation (Direct, ReAct, SoM) -- [ ] docs.openadapt.ai launch - -#### P2 - This Month -- [ ] Training dashboard in viewer -- [ ] WebArena benchmark integration -- [ ] Cloud GPU training (Lambda Labs) -- [ ] v1.0.0 meta-package release - -#### P3 - Future -- [ ] Process mining / abstraction -- [ ] Self-improvement from execution traces -- [ ] Multi-agent collaboration -- [ ] Active learning with human feedback -- [ ] OSWorld benchmark integration - ---- - -## 8. Architecture Diagrams - -### Master Architecture Diagram (Evolved) - -This diagram synthesizes the three-phase pipeline with all key concepts: demo-conditioned prompting, policy/grounding separation, safety gate, multi-source data ingestion, the abstraction ladder, and evaluation-driven feedback loops. - -```mermaid -flowchart TB - %% ═══════════════════════════════════════════════════════════════════════ - %% USER LAYER - %% ═══════════════════════════════════════════════════════════════════════ - subgraph UserLayer["User Layer"] - CLI["openadapt CLI"] - UI["Desktop/Web GUI"] - end - - %% ═══════════════════════════════════════════════════════════════════════ - %% MULTI-SOURCE DATA INGESTION - %% ═══════════════════════════════════════════════════════════════════════ - subgraph DataSources["Multi-Source Data Ingestion"] - direction LR - HUMAN["Human
Demonstrations"] - SYNTH["Synthetic
Data"]:::future - BENCH_DATA["Benchmark
Tasks"] - EXTERNAL["External
Datasets"]:::future - end - - %% ═══════════════════════════════════════════════════════════════════════ - %% PHASE 1: DEMONSTRATE (Observation Collection) - %% ═══════════════════════════════════════════════════════════════════════ - subgraph Phase1["DEMONSTRATE (Observation Collection)"] - direction TB - - subgraph CaptureLayer["Capture"] - REC["Recorder
openadapt-capture"] - A11Y["A11y Tree"] - SCREENSHOT["Screenshots"] - EVENTS["Input Events"] - - REC --> A11Y - REC --> SCREENSHOT - REC --> EVENTS - end - - subgraph PrivacyLayer["Privacy"] - SCRUB["Scrubber
openadapt-privacy"] - REDACT["PII/PHI Redaction"] - SCRUB --> REDACT - end - - STORE[("Demo Library
(JSON/Parquet)")] - - A11Y --> SCRUB - SCREENSHOT --> SCRUB - EVENTS --> SCRUB - REDACT --> STORE - end - - %% ═══════════════════════════════════════════════════════════════════════ - %% PHASE 2: LEARN (Policy Acquisition) - %% ═══════════════════════════════════════════════════════════════════════ - subgraph Phase2["LEARN (Policy Acquisition)"] - direction TB - - subgraph RetrievalPath["Path A: Retrieval-Augmented Prompting"] - EMB["Embedder
openadapt-retrieval"] - IDX[("Vector Index")] - SEARCH["Similarity Search"] - - EMB --> IDX - IDX --> SEARCH - end - - subgraph TrainingPath["Path B: Fine-Tuning"] - LOADER["Data Loader"] - TRAINER["Model Trainer
openadapt-ml"] - LORA["LoRA Adapters"] - CKPT[("Model Checkpoints")] - - LOADER --> TRAINER - TRAINER --> LORA - LORA --> CKPT - end - - subgraph MiningPath["Path C: Process Mining"]:::futureBlock - ABSTRACT["Abstractor"]:::future - PATTERNS["Pattern Library"]:::future - - ABSTRACT --> PATTERNS - end - end - - %% ═══════════════════════════════════════════════════════════════════════ - %% PHASE 3: EXECUTE (Agent Deployment) - %% ═══════════════════════════════════════════════════════════════════════ - subgraph Phase3["EXECUTE (Agent Deployment)"] - direction TB - - subgraph AgentLoop["Agent Execution Loop"] - OBS["1. OBSERVE
(Screenshot + A11y)"] - GROUND["2. GROUND
openadapt-grounding"] - PLAN["3. PLAN
(Demo-Conditioned Policy)"] - ACT["4. ACT
(Input Synthesis)"] - - OBS --> GROUND - GROUND --> PLAN - PLAN --> ACT - end - - subgraph SafetyGate["Safety Gate (Runtime Layer)"] - VALIDATE["Action Validation"] - RISK["Risk Assessment"] - CONFIRM["Human Confirm"]:::future - - VALIDATE --> RISK - RISK --> CONFIRM - end - - subgraph Evaluation["Evaluation"] - EVALS["Benchmark Runner
openadapt-evals"] - METRICS["Metrics
(Success, Steps, Time)"] - COMPARE["Model Comparison"] - - EVALS --> METRICS - METRICS --> COMPARE - end - - ACT --> VALIDATE - CONFIRM --> EVALS - end +### Feedback Loop Details - %% ═══════════════════════════════════════════════════════════════════════ - %% THE ABSTRACTION LADDER - %% ═══════════════════════════════════════════════════════════════════════ - subgraph AbstractionLadder["The Abstraction Ladder"] - direction TB - L0["Level 0: LITERAL
(Raw Events)
{ press: 'h', press: 'i' }"] - L1["Level 1: SYMBOLIC
(Semantic Actions)
{ type: 'hi bob' }"] - L2["Level 2: TEMPLATE
(Parameterized)
{ type: 'hi <name>' }"] - L3["Level 3: SEMANTIC
(Intent Recognition)
{ greet: user }"]:::future - L4["Level 4: GOAL
(Task Specification)
'Greet customer'"]:::future - - L0 -->|"Reduction"| L1 - L1 -->|"Anonymization"| L2 - L2 -.->|"Process Mining"| L3 - L3 -.->|"Goal Composition"| L4 - end - - %% ═══════════════════════════════════════════════════════════════════════ - %% MODEL LAYER (VLM Adapters) - %% ═══════════════════════════════════════════════════════════════════════ - subgraph Models["Model Layer (VLM Adapters)"] - direction LR - - subgraph CloudModels["Cloud APIs"] - CLAUDE["Claude 3.5"] - GPT["GPT-4o"] - GEMINI["Gemini 2.0"] - end - - subgraph LocalModels["Local Models"] - QWEN["Qwen2-VL"] - CUSTOM["Custom Fine-tuned"] - end - end - - %% ═══════════════════════════════════════════════════════════════════════ - %% VIEWER (Cross-Cutting) - %% ═══════════════════════════════════════════════════════════════════════ - subgraph Viewer["Cross-Cutting: Viewer"] - VIZ["Trajectory
Visualization"] - REPLAY["Demo
Replay"] - DASH["Training
Dashboard"]:::partialImpl - end +| Loop | Name | Trigger | Outcome | Status | +|------|------|---------|---------|--------| +| 1 | **Retry** | Recoverable error | Re-attempt action | **Implemented** | +| 2 | **Library Growth** | Successful execution | New demo added | **Implemented** | +| 3 | **Human-in-Loop** | Unrecoverable failure | Human correction -> demo | **Implemented** | +| 4 | **Self-Improvement** | Execution traces | Fine-tuning | **Research** | +| 5 | **Benchmark-Driven** | Eval metrics | Architecture changes | **Active** | - %% ═══════════════════════════════════════════════════════════════════════ - %% DATA FLOW CONNECTIONS - %% ═══════════════════════════════════════════════════════════════════════ - - %% User interactions - CLI --> REC - UI --> REC - CLI --> TRAINER - CLI --> EVALS - - %% Multi-source ingestion - HUMAN --> REC - SYNTH -.-> LOADER - BENCH_DATA --> EVALS - EXTERNAL -.-> LOADER +--- - %% Demo flow to learning - STORE --> EMB - STORE --> LOADER - STORE -.-> ABSTRACT - - %% ═══════════════════════════════════════════════════════════════════════ - %% DEMO-CONDITIONED PROMPTING (Core Innovation) - %% Retrieval used in BOTH training AND evaluation - %% ═══════════════════════════════════════════════════════════════════════ - SEARCH -->|"demo context
(training)"| PLAN - SEARCH -->|"demo context
(evaluation)"| EVALS - CKPT -->|"trained policy"| PLAN - PATTERNS -.->|"templates"| PLAN - - %% Model connections (Policy/Grounding Separation) - PLAN -->|"action prediction"| Models - GROUND -->|"element localization"| Models - - %% ═══════════════════════════════════════════════════════════════════════ - %% EVALUATION-DRIVEN FEEDBACK LOOPS - %% ═══════════════════════════════════════════════════════════════════════ - METRICS -->|"success traces
(new demos)"| STORE - METRICS -.->|"training signal
(self-improvement)"| TRAINER - COMPARE -->|"failure analysis"| UserLayer +## 8. Implementation Status - %% Viewer connections - STORE -.-> VIZ - STORE -.-> REPLAY - CKPT -.-> DASH - METRICS -.-> DASH +### What's Implemented vs Future Work - %% ═══════════════════════════════════════════════════════════════════════ - %% STYLING - %% ═══════════════════════════════════════════════════════════════════════ - - %% Layer colors - classDef userLayer fill:#E74C3C,stroke:#A93226,color:#fff - classDef dataSource fill:#16A085,stroke:#0E6655,color:#fff - classDef phase1 fill:#3498DB,stroke:#1A5276,color:#fff - classDef phase2 fill:#27AE60,stroke:#1E8449,color:#fff - classDef phase3 fill:#9B59B6,stroke:#6C3483,color:#fff - classDef models fill:#F39C12,stroke:#B7950B,color:#fff - classDef viewer fill:#1ABC9C,stroke:#148F77,color:#fff - classDef safetyGate fill:#E74C3C,stroke:#922B21,color:#fff - - %% Implementation status - classDef implemented fill:#2ECC71,stroke:#1E8449,color:#fff - classDef partialImpl fill:#F4D03F,stroke:#B7950B,color:#000 - classDef future fill:#95A5A6,stroke:#707B7C,color:#fff,stroke-dasharray: 5 5 - classDef futureBlock fill:#EAECEE,stroke:#95A5A6,stroke-dasharray: 5 5 - - %% Apply layer styles - class CLI,UI userLayer - class HUMAN,BENCH_DATA dataSource - class REC,A11Y,SCREENSHOT,EVENTS,SCRUB,REDACT,STORE phase1 - class EMB,IDX,SEARCH,LOADER,TRAINER,LORA,CKPT phase2 - class OBS,GROUND,PLAN,ACT,VALIDATE,RISK,EVALS,METRICS,COMPARE phase3 - class CLAUDE,GPT,GEMINI,QWEN,CUSTOM models - class VIZ,REPLAY viewer - - %% Apply abstraction ladder styles (implemented vs future) - class L0,L1,L2 implemented +``` ++==============================================================================+ +| IMPLEMENTED (Solid) | ++==============================================================================+ +| Component | Package | Notes | ++--------------------------+------------------+--------------------------------+ +| Screen capture | capture | macOS, Windows, Linux | +| Event recording | capture | Mouse, keyboard, touch | +| Event aggregation | capture | Literal -> Symbolic | +| A11y tree capture | capture | macOS, Windows | +| Demo storage | capture | JSON/Parquet/PNG | +| Privacy scrubbing | privacy | Presidio, AWS Comprehend | +| Demo embedding | retrieval | CLIP, SigLIP | +| Vector indexing | retrieval | FAISS, Annoy | +| Similarity search | retrieval | Top-k retrieval | +| API model adapters | ml | Claude, GPT-4V, Gemini | +| Element detection | grounding | OmniParser, Florence2 | +| SoM annotation | grounding | Numbered element labels | +| WAA benchmark | evals | Full integration | +| Mock benchmark | evals | Testing infrastructure | +| HTML visualization | viewer | Trajectory replay | +| Unified CLI | openadapt | capture/train/eval/view | ++==============================================================================+ + ++==============================================================================+ +| IN PROGRESS (Dashed) | ++==============================================================================+ +| Component | Package | Notes | ++--------------------------+------------------+--------------------------------+ +| Training pipeline | ml | Qwen-VL fine-tuning | +| LoRA adapters | ml | Parameter-efficient training | +| Template extraction | capture | Regex-based parameterization | +| WebArena benchmark | evals | Browser automation | +| Training dashboard | viewer | Loss/metrics visualization | +| Audio transcription | capture | Whisper integration | ++--------------------------+------------------+--------------------------------+ + ++==============================================================================+ +| FUTURE WORK (Dotted) | ++==============================================================================+ +| Component | Package | Notes | ++--------------------------+------------------+--------------------------------+ +| Process mining | ml (future) | Semantic action discovery | +| Goal composition | ml (future) | High-level task planning | +| Self-improvement | ml (future) | Training on execution traces | +| OSWorld benchmark | evals | Cross-platform desktop | +| Multi-agent collaboration| ml (future) | Agent coordination | +| Active learning | ml (future) | Human feedback integration | +| Mobile platform | capture | iOS, Android | +| Safety gates | evals | Action validation layer | ++==============================================================================+ ``` -### Key Architectural Insights - -#### 1. Demo-Conditioned Prompting (Core Innovation) - -The diagram shows how **retrieval** feeds into BOTH: -- **Training path**: Similar demos condition the fine-tuning process -- **Evaluation path**: Retrieved demos provide in-context examples for API agents - -This "show, don't tell" approach improves first-action accuracy from 33% to 100%. - -#### 2. Policy/Grounding Separation +### Abstraction Ladder Implementation Status -The EXECUTE phase clearly separates: -- **Policy** (PLAN): Decides *what* action to take (uses VLM reasoning) -- **Grounding**: Determines *where* to execute (UI element localization via SoM, OmniParser, etc.) +| Level | Name | Status | Implementation | +|-------|------|--------|----------------| +| 0 | Literal | **Implemented** | Raw event recording in `capture` | +| 1 | Symbolic | **Implemented** | Event aggregation in `capture` | +| 2 | Template | **Partial** | Regex extraction in `capture` | +| 3 | Semantic | **Research** | LLM intent recognition | +| 4 | Goal | **Future** | Process mining | -#### 3. Safety Gate as Runtime Layer +--- -Before action execution, the Safety Gate provides: -- Action validation (sanity checks) -- Risk assessment (destructive action detection) -- Human confirmation (future: for high-risk actions) +## 9. Architecture Evolution Diagrams -#### 4. The Abstraction Ladder +### Era 1: Alpha Monolith (2023) -Progressive generalization from raw events to goals: -- **Implemented**: Literal -> Symbolic -> Template -- **Future**: Semantic -> Goal (requires process mining) +``` ++=========================================================================+ +| ALPHA ARCHITECTURE (2023) | ++=========================================================================+ +| | +| +------------------------------------------------------------------+ | +| | openadapt (monolithic) | | +| +------------------------------------------------------------------+ | +| | | | +| | +-------------+ +-------------+ +-------------+ | | +| | | record | -> | visualize | -> | replay | | | +| | +-------------+ +-------------+ +-------------+ | | +| | | | | | | +| | v v v | | +| | +-------------+ +-------------+ +------------------+ | | +| | | models | | plotting | | strategies/ | | | +| | | - Recording | | - HTML gen | | - base.py | | | +| | | - ActionEvt | | | | - naive.py | | | +| | | - Screenshot| | | | - vanilla.py | | | +| | | - WindowEvt | | | | - visual.py | | | +| | +-------------+ +-------------+ +------------------+ | | +| | | | | | +| | v v | | +| | +-------------+ +---------------+ | | +| | | db/ | | adapters/ | | | +| | | - SQLite | | - anthropic | | | +| | | - CRUD ops | | - openai | | | +| | +-------------+ | - replicate | | | +| | +---------------+ | | +| +------------------------------------------------------------------+ | +| | ++=========================================================================+ + +Characteristics: +- Single repository, single package +- Tightly coupled components +- Strategy pattern for replay variants +- SQLite + Alembic migrations +- Prompts embedded in code +``` -#### 5. Evaluation-Driven Feedback Loops +### Era 2: Transition (2024) -Three feedback mechanisms: -1. **Demo Library Growth**: Success traces become new training data -2. **Self-Improvement**: Training signal from execution metrics (future) -3. **Failure Analysis**: Human review of failed executions +``` ++=========================================================================+ +| TRANSITION ARCHITECTURE (2024) | ++=========================================================================+ +| | +| Legacy codebase frozen -> /legacy/ | +| | +| New modular packages designed: | +| | +| +-------------+ +-------------+ +-------------+ +-------------+ | +| | capture | | ml | | evals | | viewer | | +| +-------------+ +-------------+ +-------------+ +-------------+ | +| | privacy | | retrieval | | grounding | | +| +-------------+ +-------------+ +-------------+ | +| | +| Key changes: | +| - Separate PyPI packages | +| - Lazy imports for optional deps | +| - Unified CLI in meta-package | +| - Policy/grounding separation | +| - Benchmark-first development | +| | ++=========================================================================+ +``` ---- +### Era 3: Modern Meta-Package (2025+) -### Legacy Master Architecture Diagram +``` ++=========================================================================+ +| MODERN ARCHITECTURE (2025+) | ++=========================================================================+ +| | +| +------------------+ | +| | User Layer | | +| | CLI / Web UI | | +| +--------+---------+ | +| | | +| v | +| +------------------+ | +| | openadapt | | +| | (meta-package) | | +| +--------+---------+ | +| | | +| +------------------------+------------------------+ | +| | | | | | | +| v v v v v | +| +---------+ +---------+ +---------+ +---------+ +--------+ | +| | capture | | ml | | evals | | viewer | |optional| | +| +---------+ +---------+ +---------+ +---------+ +--------+ | +| | | | | | | +| v v v v v | +| +---------------------------------------------------------------+ | +| | Shared Interfaces | | +| | - Trajectory format (JSON/Parquet) | | +| | - Action space specification | | +| | - Observation schema | | +| | - Benchmark protocols | | +| +---------------------------------------------------------------+ | +| | | +| v | +| +---------------------------------------------------------------+ | +| | Model Layer | | +| | +----------+ +----------+ +----------+ +----------+ | | +| | | Claude | | GPT-4V | | Gemini | | Qwen-VL | | | +| | +----------+ +----------+ +----------+ +----------+ | | +| +---------------------------------------------------------------+ | +| | ++=========================================================================+ +``` -For reference, the previous architecture diagram: +### Full System Architecture (Mermaid) ```mermaid flowchart TB @@ -824,8 +815,8 @@ flowchart TB subgraph Phase1["DEMONSTRATE"] direction TB - REC[Recorder] - SCRUB[Privacy Scrubber] + REC[Recorder
openadapt-capture] + SCRUB[Privacy Scrubber
openadapt-privacy] STORE[(Demo Library)] REC --> SCRUB @@ -855,11 +846,11 @@ flowchart TB subgraph Phase3["EXECUTE"] direction TB - OBS[Observer] - GROUND[Grounder] - PLAN[Planner] - ACT[Actuator] - EVAL[Evaluator] + OBS[1. OBSERVE] + GROUND[2. GROUND
openadapt-grounding] + PLAN[3. PLAN
Demo-Conditioned] + ACT[4. ACT] + EVAL[5. EVALUATE
openadapt-evals] OBS --> GROUND GROUND --> PLAN @@ -874,7 +865,6 @@ flowchart TB GPT[GPT-4o] GEMINI[Gemini] QWEN[Qwen-VL] - CUSTOM[Custom] end subgraph Viewer["Cross-Cutting: Viewer"] @@ -900,8 +890,8 @@ flowchart TB TRAINER --> CKPT ABSTRACT --> PATTERNS - %% Execution flow - SEARCH -.->|context| PLAN + %% Execution flow (demo-conditioning) + SEARCH -->|demo context| PLAN CKPT -->|policy| PLAN PATTERNS -.->|templates| PLAN @@ -932,10 +922,51 @@ flowchart TB class EMB,IDX,SEARCH,LOADER,TRAINER,CKPT phase2 class ABSTRACT,PATTERNS future class OBS,GROUND,PLAN,ACT,EVAL phase3 - class CLAUDE,GPT,GEMINI,QWEN,CUSTOM models + class CLAUDE,GPT,GEMINI,QWEN models class VIZ,REPLAY,DASH viewer ``` +### Execution Loop Evolution + +``` +ALPHA: Strategy-Based MODERN: Policy/Grounding +================================ ================================ + ++------------------+ +------------------+ +| BaseReplay | | OBSERVE | +| Strategy | | (Screenshot + | +| | | A11y tree) | +| while True: | +--------+---------+ +| screenshot = | | +| take() | v +| action = | +------------------+ +| get_next() | ------> | GROUND | +| play(action) | | (Element detect | +| | | + SoM annotate)| ++------------------+ +--------+---------+ + | + v + +------------------+ + | PLAN | + | (VLM reasoning | + | + demo context)| + +--------+---------+ + | + v + +------------------+ + | ACT | + | (Input synth + | + | safety check) | + +--------+---------+ + | + v + +------------------+ + | EVALUATE | + | (Success check | + | + feedback) | + +------------------+ +``` + ### Package Responsibility Diagram ```mermaid @@ -1001,57 +1032,6 @@ flowchart LR class OA meta ``` -### Execution Loop Diagram - -```mermaid -stateDiagram-v2 - [*] --> Observe - - Observe --> Ground: screenshot + a11y - Ground --> Plan: elements + coordinates - Plan --> Act: action prediction - Act --> Evaluate: action result - - Evaluate --> Observe: continue - Evaluate --> Success: task complete - Evaluate --> Retry: recoverable error - Evaluate --> Escalate: unrecoverable - - Retry --> Observe - Escalate --> [*] - Success --> [*] - - note right of Observe - Capture screenshot - Extract a11y tree - Build observation - end note - - note right of Ground - Detect UI elements - Apply SoM labels - Get coordinates - end note - - note right of Plan - Encode with VLM - Retrieve similar demos - Generate action - end note - - note right of Act - Parse action type - Execute input - Record for history - end note - - note right of Evaluate - Check success - Detect failures - Decide next step - end note -``` - ### Feedback Loop Diagram ```mermaid @@ -1094,151 +1074,42 @@ flowchart TB --- -## 9. Key Design Principles - -### Principle 1: Model Agnostic - -OpenAdapt works with any VLM that can process images and generate text. - -**Implementation**: -- Adapter pattern for model integration -- Unified prompt format across providers -- Switchable at runtime via configuration - -**Rationale**: -- Avoid vendor lock-in -- Enable cost optimization -- Future-proof against model evolution - -### Principle 2: Demo-Conditioned - -Agents learn from human examples, not just prompts. - -**Implementation**: -- Retrieval-augmented prompting -- Fine-tuning on demonstration datasets -- Context windows include similar past examples - -**Rationale**: -- Captures implicit knowledge -- Reduces ambiguity -- Enables transfer learning - -### Principle 3: Abstraction-Aware - -Progress from literal replay to semantic understanding. - -**Implementation**: -- Abstraction ladder (literal -> symbolic -> template -> semantic -> goal) -- Incremental abstraction during processing -- Human-readable intermediate representations - -**Rationale**: -- Enables generalization -- Supports explanation and debugging -- Allows cross-application transfer - -### Principle 4: Evaluation-Driven - -Rigorous benchmarking on standard tasks. - -**Implementation**: -- WAA, WebArena, OSWorld benchmark integrations -- Automated regression detection -- Public leaderboard metrics - -**Rationale**: -- Objective progress measurement -- Community comparability -- Quality assurance - -### Principle 5: Privacy-First - -Optional PII/PHI scrubbing at every stage. - -**Implementation**: -- `openadapt-privacy` package -- Configurable scrubbing levels -- Local-only deployment option - -**Rationale**: -- Enterprise compliance (HIPAA, GDPR) -- User trust -- Responsible AI - -### Principle 6: Open Source - -MIT license, community-driven development. - -**Implementation**: -- All packages on GitHub -- Public roadmap and issues -- Contribution guidelines - -**Rationale**: -- Transparency -- Community innovation -- No vendor lock-in - ---- - -## 10. Research Alignment - -OpenAdapt's architecture aligns with and builds upon recent GUI agent research. - -### Key Research Papers - -| Paper | Contribution | OpenAdapt Integration | -|-------|--------------|----------------------| -| Claude Computer Use (Anthropic, 2024) | Production VLM agent API | API adapter in `openadapt-ml` | -| UFO (Microsoft, 2024) | Windows agent architecture | Prompt patterns adopted | -| OSWorld (CMU, 2024) | Cross-platform benchmark | Benchmark adapter planned | -| Set-of-Mark (Microsoft, 2023) | Visual grounding via labels | Core grounding mode | -| OmniParser (Microsoft, 2024) | Pure-vision UI parsing | Provider in `openadapt-grounding` | -| WebArena (CMU, 2023) | Web automation benchmark | Benchmark adapter implemented | -| Mind2Web (OSU, 2023) | Web action prediction | Dataset format compatible | - -### Research Contributions - -OpenAdapt contributes to the research community through: - -1. **Open Benchmark Infrastructure**: Standardized evaluation setup -2. **Demonstration Dataset Format**: Interoperable trajectory format -3. **Retrieval-Augmented Agents**: Demo conditioning research -4. **Grounding Comparison**: Multi-provider benchmarks -5. **Abstraction Research**: Process mining for GUI agents - ---- - -## 11. Future Directions +## 10. Future Directions ### Near-Term (Q1 2026) -- Complete fine-tuning pipeline validation -- Achieve competitive WAA benchmark scores -- Launch docs.openadapt.ai -- Release v1.0.0 meta-package +| Priority | Goal | Package | Status | +|----------|------|---------|--------| +| P0 | PyPI releases for all packages | all | In progress | +| P0 | WAA baseline metrics established | evals | Pending | +| P1 | Fine-tuning pipeline validated | ml | In progress | +| P1 | Demo conditioning in evals | evals + retrieval | Pending | +| P2 | docs.openadapt.ai launched | docs | Pending | ### Medium-Term (2026) -- Process mining implementation -- Self-improvement loop activation -- Multi-benchmark evaluation suite -- Enterprise deployment guides +| Goal | Description | +|------|-------------| +| **Process Mining** | Automatic extraction of semantic actions from demos | +| **Self-Improvement** | Training on successful execution traces | +| **Multi-Benchmark** | WebArena + OSWorld integration | +| **Enterprise Deployment** | Production deployment guides | ### Long-Term (2026+) -- Multi-agent collaboration -- Active learning with human feedback -- Mobile platform support -- Cross-platform transfer learning +| Goal | Description | +|------|-------------| +| **Cross-App Transfer** | Demos from Excel help with Google Sheets | +| **Multi-Agent** | Coordinated agents for complex workflows | +| **Active Learning** | Agents request human help strategically | +| **Mobile Platforms** | iOS and Android capture/replay | -### Research Agenda +### Research Questions -1. **Abstraction Hierarchy**: Can we automatically extract semantic actions from demonstrations? -2. **Transfer Learning**: How do demos from one app help in another? -3. **Active Learning**: When should the agent ask for human help? -4. **Explanation**: How do we make agent decisions interpretable? +1. **Abstraction Discovery**: Can we automatically extract semantic actions from literal event sequences? +2. **Transfer Learning**: How much does demo conditioning help across different applications? +3. **Explanation**: How do we make agent decisions interpretable to users? +4. **Safety**: What guardrails prevent harmful autonomous actions? --- @@ -1246,12 +1117,13 @@ OpenAdapt contributes to the research community through: | Term | Definition | |------|------------| -| **A11y Tree** | Accessibility tree - structured representation of UI elements | +| **A11y Tree** | Accessibility tree - structured UI element representation | | **Demo** | Recorded human demonstration (trajectory) | -| **Grounding** | Mapping text descriptions to UI coordinates | +| **Grounding** | Mapping text/intent to specific UI coordinates | | **LoRA** | Low-Rank Adaptation - efficient fine-tuning method | -| **SoM** | Set-of-Mark - visual grounding via element labels | -| **Trajectory** | Sequence of observations and actions | +| **Policy** | Decision function mapping observations to actions | +| **SoM** | Set-of-Mark - visual grounding via numbered labels | +| **Trajectory** | Sequence of (observation, action) pairs | | **VLM** | Vision-Language Model | | **WAA** | Windows Agent Arena benchmark | @@ -1259,14 +1131,15 @@ OpenAdapt contributes to the research community through: - [Architecture Overview](./architecture.md) - Package structure and data flow - [Roadmap Priorities](./roadmap-priorities.md) - Current development priorities -- [Telemetry Design](./design/telemetry-design.md) - Telemetry implementation -- [Landing Page Strategy](./design/landing-page-strategy.md) - Messaging and positioning +- [Package Documentation](./packages/index.md) - Individual package guides +- [Legacy Freeze](./legacy/freeze.md) - Migration from monolith ## Appendix C: Version History | Version | Date | Changes | |---------|------|---------| -| 2.0 | Jan 2026 | Comprehensive redesign, SOTA alignment | +| 3.0 | Jan 2026 | Alpha vision synthesis, evolution diagrams, SOTA alignment | +| 2.0 | Jan 2026 | Comprehensive redesign, modular architecture | | 1.0 | Dec 2025 | Initial modular architecture | | 0.x | 2023-2024 | Legacy monolithic design | diff --git a/docs/architecture.md b/docs/architecture.md index 6604fbb5e..664eddcdc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -79,26 +79,26 @@ flowchart TB ```mermaid flowchart LR - subgraph Record["1. Record"] - A[User Demo] --> B[Capture Session] - B --> C[Screenshots + Events] + subgraph Demonstrate["1. Demonstrate"] + A[Human Trajectory] --> B[Capture Session] + B --> C[Observations + Actions] end subgraph Store["2. Store"] C --> D[JSON/Parquet Files] - D --> E[Demo Library] + D --> E[Demonstration Library] end - subgraph Train["3. Train"] - E --> F[Data Loading] - F --> G[Model Training] + subgraph Learn["3. Learn"] + E --> F[Trajectory Abstraction] + F --> G[Policy Learning] G --> H[Checkpoint] end - subgraph Deploy["4. Deploy"] - H --> I[Agent Policy] + subgraph Execute["4. Execute"] + H --> I[Trained Policy] I --> J[Inference] - J --> K[Action Replay] + J --> K[Agent Deployment] end subgraph Evaluate["5. Evaluate"] @@ -164,17 +164,17 @@ graph TD | Package | Responsibility | Key Exports | |---------|---------------|-------------| -| **openadapt-capture** | GUI recording, event capture, storage | `CaptureSession`, `Recorder`, `Action` | -| **openadapt-ml** | Model training, inference, adapters | `QwenVLAdapter`, `Trainer`, `AgentPolicy` | +| **openadapt-capture** | Demonstration collection, observation-action capture, storage | `CaptureSession`, `Recorder`, `Action` | +| **openadapt-ml** | Policy learning, training, inference | `QwenVLAdapter`, `Trainer`, `AgentPolicy` | | **openadapt-evals** | Benchmark evaluation, metrics | `ApiAgent`, `BenchmarkAdapter`, `evaluate_agent_on_benchmark` | -| **openadapt-viewer** | HTML visualization, replay viewer | `PageBuilder`, `HTMLBuilder` | +| **openadapt-viewer** | Trajectory visualization | `PageBuilder`, `HTMLBuilder` | ### Optional Packages | Package | Responsibility | Use Case | |---------|---------------|----------| -| **openadapt-grounding** | UI element localization | Improved click accuracy with element detection | -| **openadapt-retrieval** | Multimodal demo search | Find similar demonstrations for few-shot prompting | +| **openadapt-grounding** | UI element grounding | Improved action accuracy with element detection | +| **openadapt-retrieval** | Multimodal trajectory search | Find similar demonstrations for few-shot policy learning | | **openadapt-privacy** | PII/PHI scrubbing | Redact sensitive data before storage/training | ## Evaluation Loop @@ -275,14 +275,14 @@ graph LR pip install openadapt # Individual packages -pip install openadapt[capture] # GUI capture/recording -pip install openadapt[ml] # ML training and inference +pip install openadapt[capture] # Demonstration collection +pip install openadapt[ml] # Policy learning and inference pip install openadapt[evals] # Benchmark evaluation -pip install openadapt[viewer] # HTML visualization +pip install openadapt[viewer] # Trajectory visualization # Optional packages -pip install openadapt[grounding] # UI element localization -pip install openadapt[retrieval] # Demo search/retrieval +pip install openadapt[grounding] # UI element grounding +pip install openadapt[retrieval] # Trajectory retrieval pip install openadapt[privacy] # PII/PHI scrubbing # Bundles diff --git a/docs/cli.md b/docs/cli.md index 996cbf21b..02ac8bdc4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -42,11 +42,11 @@ This verifies: ## Capture Commands -Commands for recording user demonstrations. +Commands for collecting human demonstrations. ### capture start -Start a new recording session. +Start a new demonstration collection session. ```bash openadapt capture start --name [options] @@ -64,25 +64,25 @@ openadapt capture start --name [options] **Examples:** ```bash -# Basic recording +# Basic demonstration collection openadapt capture start --name login-task -# Recording without screenshots +# Demonstration collection without screenshots openadapt capture start --name audio-task --no-screenshots -# Recording with slower screenshot interval +# Demonstration collection with slower screenshot interval openadapt capture start --name slow-task --interval 1.0 ``` ### capture stop -Stop the current recording. +Stop the current demonstration collection. ```bash openadapt capture stop ``` -Alternatively, press `Ctrl+C` in the recording terminal. +Alternatively, press `Ctrl+C` in the capture terminal. ### capture list @@ -103,7 +103,7 @@ form-fill 89 5m 42s 2026-01-14 ### capture view -Open the viewer for a capture. +Open the trajectory viewer for a demonstration. ```bash openadapt capture view [options] @@ -113,13 +113,13 @@ openadapt capture view [options] | Argument | Required | Description | |----------|----------|-------------| -| `` | Yes | Name of the capture to view | +| `` | Yes | Name of the demonstration to view | | `--port` | No | Server port (default: 8080) | | `--no-browser` | No | Don't open browser automatically | ### capture delete -Delete a capture. +Delete a demonstration. ```bash openadapt capture delete @@ -129,11 +129,11 @@ openadapt capture delete ## Train Commands -Commands for training ML models. +Commands for policy learning from demonstrations. ### train start -Start training a model on a capture. +Start policy learning from a demonstration. ```bash openadapt train start --capture --model [options] @@ -143,7 +143,7 @@ openadapt train start --capture --model [options] | Argument | Required | Description | |----------|----------|-------------| -| `--capture` | Yes | Name of the capture to train on | +| `--capture` | Yes | Name of the demonstration to train on | | `--model` | Yes | Model architecture | | `--epochs` | No | Number of training epochs (default: 10) | | `--batch-size` | No | Batch size (default: 4) | @@ -159,10 +159,10 @@ openadapt train start --capture --model [options] **Examples:** ```bash -# Basic training +# Basic policy learning openadapt train start --capture login-task --model qwen3vl-2b -# Training with custom parameters +# Policy learning with custom parameters openadapt train start \ --capture login-task \ --model qwen3vl-7b \ @@ -173,7 +173,7 @@ openadapt train start \ ### train status -Check training progress. +Check policy learning progress. ```bash openadapt train status @@ -191,7 +191,7 @@ ETA: 15 minutes ### train stop -Stop the current training. +Stop the current policy learning. ```bash openadapt train stop diff --git a/docs/design/landing-page-strategy.md b/docs/design/landing-page-strategy.md new file mode 100644 index 000000000..ca6e31556 --- /dev/null +++ b/docs/design/landing-page-strategy.md @@ -0,0 +1,712 @@ +# OpenAdapt.ai Landing Page Strategy + +**Document Version**: 1.0 +**Date**: January 2026 +**Author**: Generated with AI assistance +**Status**: Proposal for Review + +--- + +## Table of Contents + +1. [Current State Analysis](#1-current-state-analysis) +2. [Target Audience Definitions](#2-target-audience-definitions) +3. [Core Messaging Strategy](#3-core-messaging-strategy) +4. [Competitive Positioning](#4-competitive-positioning) +5. [Page Section Recommendations](#5-page-section-recommendations) +6. [Copy Suggestions](#6-copy-suggestions) +7. [Wireframe Concepts](#7-wireframe-concepts) +8. [Social Proof Strategy](#8-social-proof-strategy) +9. [Call-to-Action Strategy](#9-call-to-action-strategy) +10. [Implementation Priorities](#10-implementation-priorities) + +--- + +## 1. Current State Analysis + +### 1.1 What OpenAdapt IS Today + +OpenAdapt has evolved from a monolithic application (v0.46.0) to a **modular meta-package architecture** (v1.0+). This is a significant architectural maturation that should be reflected in messaging. + +**Core Value Proposition (Current Reality)**: +- The **open** source software **adapt**er between Large Multimodal Models (LMMs) and desktop/web GUIs +- Record demonstrations, train models, evaluate agents via unified CLI +- Works with any VLM: Claude, GPT-4V, Gemini, Qwen, or custom fine-tuned models + +**Technical Differentiators (Verified)**: +1. **Model Agnostic**: Not locked to one AI provider +2. **Demo-Prompted, Not User-Prompted**: Learn from human demonstration, not complex prompt engineering +3. **Universal GUI Support**: Native apps, web browsers, virtualized environments +4. **Open Source (MIT License)**: Full transparency, no vendor lock-in + +**Key Innovation**: +- **Trajectory-conditioned disambiguation of UI affordances** - validated experiment showing 33% -> 100% first-action accuracy with demo conditioning +- **Set-of-Marks (SoM) mode**: 100% accuracy on synthetic benchmarks using element IDs instead of coordinates + +### 1.2 Current Landing Page Assessment + +**What's Working Well**: +- Clean, professional design with dark theme +- Video demo at hero section +- GitHub star/fork buttons for social proof +- Platform-specific installation instructions (auto-detects OS) +- PyPI download statistics showing traction +- Industry use cases grid (HR, Law, Insurance, etc.) +- Email signup for updates + +**What's Missing or Unclear**: +1. **No clear "what is this?"** - Visitors need to watch a video to understand +2. **Tagline "AI for Desktops" is vague** - Doesn't differentiate from competitors +3. **No comparison to alternatives** - Why choose OpenAdapt over Anthropic Computer Use? +4. **No technical credibility indicators** - No benchmark scores, no research citations +5. **Industry grid is generic** - Same features could apply to any automation tool +6. **No developer/researcher angle** - Focuses only on end-user automation +7. **Architecture transition is hidden** - v1.0+ modular design is a major selling point +8. **No clear "Who is this for?"** - Tries to appeal to everyone, resonates with no one + +**Carousel Messages Analysis**: +- "Show, don't tell." - Good but cryptic +- "Perform, don't prompt." - Best differentiator, should be prominent +- "Record, replay, and share." - Functional but not compelling + +### 1.3 Technical Accuracy Issues + +The current site doesn't reflect: +- The modular package architecture (7 focused sub-packages) +- The evaluation infrastructure (WAA, WebArena benchmarks) +- The ML training capabilities (VLM fine-tuning, LoRA) +- The retrieval-augmented prompting (demo library search) +- The privacy scrubbing capabilities (PII/PHI redaction) + +--- + +## 2. Target Audience Definitions + +### 2.1 Primary Audiences + +#### A. Developers Building Automation Agents + +**Profile**: +- Building AI-powered tools that interact with GUIs +- May be creating internal tools, startup products, or client solutions +- Comfortable with Python, CLI tools, ML concepts +- Want flexibility, not black-box solutions + +**Pain Points**: +- API-only agents (Claude Computer Use) lack customization +- Building from scratch is too slow +- Need to run locally for privacy/security +- Want to fine-tune models on specific workflows + +**What They Need to See**: +- Clear architecture diagrams +- Code examples (pip install, quick start) +- Benchmark scores vs. alternatives +- Extensibility points (adapters, plugins) + +**Key Message**: "The open source SDK for building GUI automation agents" + +#### B. Enterprise Process Automation Buyers + +**Profile**: +- Looking to automate repetitive knowledge work +- Concerned about security, privacy, compliance +- Need to justify ROI and integrate with existing systems +- Often have IT/security review requirements + +**Pain Points**: +- Existing RPA is brittle and expensive to maintain +- Cloud-only AI raises data privacy concerns +- Need clear enterprise support options +- Require compliance with industry regulations + +**What They Need to See**: +- Privacy features (PII/PHI scrubbing) +- On-premise deployment options +- Enterprise support/contact information +- Industry-specific use case studies +- Security architecture information + +**Key Message**: "AI-first automation that runs where your data lives" + +#### C. ML Researchers Studying GUI Agents + +**Profile**: +- Academic researchers or industry R&D teams +- Working on VLM capabilities, agent architectures, benchmarks +- Need reproducible baselines and evaluation infrastructure +- Want to contribute to or build upon open research + +**Pain Points**: +- Existing benchmarks are hard to set up +- Need standardized evaluation metrics +- Want to compare models fairly +- Limited open-source alternatives to proprietary agent frameworks + +**What They Need to See**: +- Benchmark integration (WAA, WebArena, OSWorld) +- Published metrics and methodology +- Research paper citations (if any) +- Clear contribution pathways +- Schema/data format documentation + +**Key Message**: "Open infrastructure for GUI agent research and benchmarking" + +#### D. ML Engineers Interested in VLM Fine-Tuning + +**Profile**: +- Want to train custom models for specific GUI tasks +- Familiar with training infrastructure (LoRA, PEFT, etc.) +- Looking for training data and pipelines +- Want efficient local or cloud training options + +**Pain Points**: +- Collecting GUI interaction data is tedious +- Setting up VLM training pipelines is complex +- Need baselines to compare against +- Cloud GPU costs add up quickly + +**What They Need to See**: +- Training pipeline documentation +- Supported models (Qwen3-VL, etc.) +- Training results (before/after fine-tuning) +- Cloud GPU integration (Lambda Labs, Azure) +- Data format specifications + +**Key Message**: "Record demonstrations, train specialized GUI agents" + +### 2.2 Audience Prioritization + +For the landing page, prioritize in this order: +1. **Developers** (highest volume, most likely to convert to users/contributors) +2. **Enterprise buyers** (revenue potential, require dedicated section) +3. **ML engineers** (overlaps with developers, training angle) +4. **Researchers** (smaller audience, but important for credibility) + +--- + +## 3. Core Messaging Strategy + +### 3.1 Primary Tagline Options + +**Option A (Recommended)**: +> **"Teach AI to use any software."** + +Why: Simple, benefit-focused, implies the key differentiator (demonstration-based learning) + +**Option B**: +> **"The open source adapter between AI and any GUI."** + +Why: Explains the technical position, highlights open source + +**Option C**: +> **"Perform, don't prompt."** + +Why: Clever contrast to prompt engineering, memorable + +**Option D**: +> **"Record. Train. Automate."** + +Why: Clear 3-step process, action-oriented + +### 3.2 Supporting Taglines (Subheadlines) + +- "Show AI how to do a task once. Let it handle the rest." +- "From human demonstration to AI automation in minutes." +- "Open source GUI automation with the AI model of your choice." +- "Works with Claude, GPT-4V, Gemini, Qwen, or your own fine-tuned models." + +### 3.3 Key Differentiators to Emphasize + +1. **Demonstration-Based Learning** + - Not: "Use natural language to describe tasks" + - But: "Just do the task and OpenAdapt learns from watching" + - Proof: 33% -> 100% first-action accuracy with demo conditioning + +2. **Model Agnostic** + - Not: "Works with [specific AI]" + - But: "Your choice: Claude, GPT-4V, Gemini, Qwen, or custom models" + - Proof: Adapters for multiple VLM backends + +3. **Runs Anywhere** + - Not: "Cloud-powered automation" + - But: "Run locally, in the cloud, or hybrid" + - Proof: CLI-based, works offline + +4. **Open Source** + - Not: "Try our free tier" + - But: "MIT licensed, fully transparent, community-driven" + - Proof: GitHub, PyPI, active Discord + +### 3.4 Messaging Framework + +**For Developers**: +> "Build GUI automation agents with a modular Python SDK. Record demonstrations, train models, evaluate on benchmarks. Works with any VLM." + +**For Enterprise**: +> "AI-first process automation that learns from your team. Privacy-first architecture with PII/PHI scrubbing. Deploy where your data lives." + +**For Researchers**: +> "Open infrastructure for GUI agent research. Standardized benchmarks, reproducible baselines, extensible architecture." + +**For ML Engineers**: +> "Fine-tune VLMs on real GUI workflows. Record data, train with LoRA, evaluate accuracy. Local or cloud training." + +--- + +## 4. Competitive Positioning + +### 4.1 Primary Competitors + +| Competitor | Strengths | Weaknesses | Our Advantage | +|------------|-----------|------------|---------------| +| **Anthropic Computer Use** | First-mover, Claude integration, simple API | Proprietary, cloud-only, no customization | Open source, model-agnostic, trainable | +| **UI-TARS (ByteDance)** | Strong benchmark scores, research backing | Closed source, not productized | Open source, deployable, extensible | +| **Traditional RPA (UiPath, etc.)** | Enterprise-proven, large ecosystems | Brittle selectors, no AI reasoning, expensive | AI-first, learns from demos, affordable | +| **GPT-4V + Custom Code** | Powerful model, flexibility | Requires building everything, no structure | Ready-made SDK, training pipeline, benchmarks | + +### 4.2 Positioning Statement + +> "OpenAdapt is the **open source alternative** to proprietary GUI automation APIs. Unlike cloud-only solutions, OpenAdapt lets you **train custom models** on your workflows and **deploy anywhere**. Unlike traditional RPA, OpenAdapt uses **AI reasoning** and **learns from demonstrations** instead of brittle scripts." + +### 4.3 Comparison Talking Points + +**vs. Anthropic Computer Use**: +- "Model-agnostic - not locked to one provider" +- "Fine-tune on your specific workflows" +- "Run locally for privacy-sensitive data" +- "Open source with community contributions" + +**vs. Traditional RPA**: +- "AI understands intent, not just element selectors" +- "Adapts to UI changes without manual updates" +- "Learn from demonstrations, not scripting" +- "Fraction of the cost, faster to deploy" + +--- + +## 5. Page Section Recommendations + +### 5.1 Proposed Page Structure + +1. **Hero Section** (Above the fold) +2. **How It Works** (3-step process) +3. **Key Differentiators** (3-4 value props) +4. **For Developers** (SDK/CLI features) +5. **For Enterprise** (Security, privacy, support) +6. **Use Cases** (Specific, concrete examples) +7. **Comparison** (Why OpenAdapt) +8. **Social Proof** (Metrics, testimonials, logos) +9. **Getting Started** (Install, docs, community) +10. **Footer** (Links, legal, contact) + +### 5.2 Hero Section Redesign + +**Current**: "OpenAdapt.AI - AI for Desktops. Automate your workflows. No coding required." + +**Proposed**: + +``` +[Logo] OpenAdapt.AI + +# Teach AI to use any software. + +Show it once. Let it handle the rest. + +[Video Demo - Keep current] + +[Install in 30 seconds] [View on GitHub] [Join Discord] + +"Works with Claude, GPT-4V, Gemini, Qwen, or your own fine-tuned models" + +{GitHub Stars} {PyPI Downloads} {Discord Members} +``` + +### 5.3 How It Works Section + +**Current**: Carousel with "Show, don't tell" / "Perform, don't prompt" / "Record, replay, share" + +**Proposed**: Clear 3-step process with visuals + +``` +## How OpenAdapt Works + +1. RECORD + [Icon: Screen recording] + Demonstrate the task by doing it yourself. + OpenAdapt captures screenshots, mouse clicks, and keystrokes. + +2. TRAIN + [Icon: Neural network] + Train an AI model on your demonstration. + Fine-tune Qwen-VL, use Claude/GPT-4V, or bring your own model. + +3. DEPLOY + [Icon: Play button] + Run the trained agent to automate the task. + Evaluate with standardized benchmarks. +``` + +### 5.4 Differentiators Section + +``` +## Why OpenAdapt? + +### Demonstration-Based Learning +No prompt engineering required. OpenAdapt learns from how you actually do tasks. +[Stat: 33% -> 100% first-action accuracy with demo conditioning] + +### Model Agnostic +Your choice of AI: Claude, GPT-4V, Gemini, Qwen-VL, or fine-tune your own. +Not locked to any single provider. + +### Run Anywhere +CLI-based, works offline. Deploy locally, in the cloud, or hybrid. +Your data stays where you want it. + +### Fully Open Source +MIT licensed. Transparent, auditable, community-driven. +No vendor lock-in, ever. +``` + +### 5.5 For Developers Section + +``` +## Built for Developers + +### Modular Architecture +Seven focused packages you can install individually: +- openadapt-capture: Recording +- openadapt-ml: Training & inference +- openadapt-evals: Benchmarking +- openadapt-viewer: Visualization +- openadapt-grounding: UI element detection +- openadapt-retrieval: Demo library search +- openadapt-privacy: PII/PHI scrubbing + +### Quick Start +```bash +# Install +pip install openadapt[all] + +# Record a demonstration +openadapt capture start --name my-task + +# Train a model +openadapt train start --capture my-task --model qwen3vl-2b + +# Evaluate +openadapt eval run --checkpoint model.pt --benchmark waa +``` + +### Benchmark Ready +Integrated with Windows Agent Arena (WAA), WebArena, and OSWorld. +Compare your models against published baselines. + +[View Documentation] [GitHub Repository] +``` + +### 5.6 For Enterprise Section + +``` +## Enterprise-Ready Automation + +### Privacy First +Built-in PII/PHI scrubbing with AWS Comprehend, Microsoft Presidio, or Private AI. +Your sensitive data never leaves your infrastructure. + +### Deploy Your Way +Run entirely on-premise, in your cloud, or hybrid. +No data leaves your environment unless you want it to. + +### Compliance Ready +Audit logging, reproducible recordings, explainable AI decisions. +Built for regulated industries. + +### Enterprise Support +Custom development, training, and support packages available. + +[Contact Sales: sales@openadapt.ai] +``` + +### 5.7 Use Cases Section (Refined) + +**Current**: Generic industry grid + +**Proposed**: Specific, concrete use cases with workflows + +``` +## Real-World Automation + +### Data Entry Across Systems +Transfer information between applications that don't integrate. +Example: Copy customer data from CRM to billing system. + +### Report Generation +Compile data from multiple sources into standardized reports. +Example: Monthly sales reports from Salesforce + Excel + internal tools. + +### Legacy System Integration +Automate workflows in applications without APIs. +Example: Mainframe data entry, proprietary healthcare systems. + +### Quality Assurance Testing +Record manual test procedures, replay with validation. +Example: Regression testing across UI updates. + +### Process Documentation +Record workflows to create training materials automatically. +Example: Onboarding guides for complex internal tools. +``` + +--- + +## 6. Copy Suggestions + +### 6.1 Headlines + +| Section | Headline | Subheadline | +|---------|----------|-------------| +| Hero | "Teach AI to use any software." | "Show it once. Let it handle the rest." | +| How It Works | "Three Steps to Automation" | "Record, train, deploy." | +| Differentiators | "Why OpenAdapt?" | "Open source, model-agnostic, demonstration-based." | +| Developers | "Built for Developers" | "A modular SDK for building GUI automation agents." | +| Enterprise | "Enterprise-Ready" | "AI automation that runs where your data lives." | +| Use Cases | "Automate Any Workflow" | "From data entry to testing to legacy integration." | +| Install | "Get Started in 30 Seconds" | "One command installs everything you need." | + +### 6.2 CTAs (Calls to Action) + +| Context | Primary CTA | Secondary CTA | +|---------|-------------|---------------| +| Hero | "Get Started" | "View Demo" | +| Developers | "View Documentation" | "Star on GitHub" | +| Enterprise | "Contact Sales" | "Download Whitepaper" | +| Footer | "Join Discord" | "View on GitHub" | + +### 6.3 Proof Points to Include + +- "33% -> 100% first-action accuracy with demonstration conditioning" +- "[X,XXX] PyPI downloads this month" (dynamic) +- "[XXX] GitHub stars" (dynamic) +- "7 modular packages, 1 unified CLI" +- "Integrated with Windows Agent Arena, WebArena, OSWorld benchmarks" +- "MIT licensed, fully open source" + +--- + +## 7. Wireframe Concepts + +### 7.1 Desktop Layout + +``` ++------------------------------------------------------------------+ +| [Logo] [Docs] [GitHub] [Discord] [Enterprise] | ++------------------------------------------------------------------+ +| | +| # Teach AI to use any software. | +| Show it once. Let it handle the rest. | +| | +| [==================== Video Demo ====================] | +| | +| [Get Started] [View on GitHub] | +| | +| Works with: [Claude] [GPT-4V] [Gemini] [Qwen] [Custom] | +| | +| [GitHub Stars] [PyPI Downloads] [Discord Members] | +| | ++------------------------------------------------------------------+ +| | +| ## How OpenAdapt Works | +| | +| [1. RECORD] [2. TRAIN] [3. DEPLOY] | +| [Screenshot] [Neural Net] [Automation] | +| Demonstrate Train on your Run the agent | +| the task. demonstration. to automate. | +| | ++------------------------------------------------------------------+ +| | +| ## Why OpenAdapt? | +| | +| [Demo-Based] [Model Agnostic] [Run Anywhere] [Open Source] | +| Learn from Your choice of Local, cloud, MIT licensed | +| examples. AI provider. or hybrid. forever. | +| | ++------------------------------------------------------------------+ +| | +| [For Developers Tab] [For Enterprise Tab] [For Researchers Tab]| +| | +| Content switches based on selected audience... | +| | ++------------------------------------------------------------------+ +| | +| ## Get Started | +| | +| [macOS] [Windows] [Linux] | +| | +| $ curl -LsSf https://astral.sh/uv/install.sh | sh | +| $ uv tool install openadapt | +| $ openadapt --help | +| | +| [X,XXX installs this month] | +| | ++------------------------------------------------------------------+ +| | +| [Footer: Links, Social, Legal] | +| | ++------------------------------------------------------------------+ +``` + +### 7.2 Mobile Considerations + +- Stack hero elements vertically +- Collapse model logos into scrollable row +- Use accordion for audience tabs +- Keep video demo prominent +- Simplify code blocks (single command with copy button) + +--- + +## 8. Social Proof Strategy + +### 8.1 Metrics to Display + +**Live Metrics** (fetch from APIs): +- GitHub stars (currently showing, keep) +- PyPI downloads per month (currently showing, keep) +- Discord member count (add if available) +- Number of GitHub contributors (add) + +**Static Metrics** (update manually): +- "7 modular packages" +- "100% synthetic benchmark accuracy (SoM mode)" +- "3 benchmark integrations (WAA, WebArena, OSWorld)" + +### 8.2 Testimonials Strategy + +**Priority Order**: +1. Named enterprise user quotes (if available) +2. Named developer testimonials from Discord +3. Anonymous industry testimonials +4. Community member quotes + +**Template for Gathering**: +> "How has OpenAdapt helped you? Reply to be featured on our website." + +### 8.3 Logo Wall + +**Target logos to seek permission for**: +- Companies using OpenAdapt in production +- Universities using for research +- Partner organizations + +**Fallback** (if no logos available): +- Featured in media logos (if covered) +- Integration partner logos (AWS, Azure, etc.) +- "Trusted by teams at Fortune 500 companies" (if true) + +--- + +## 9. Call-to-Action Strategy + +### 9.1 Primary Conversion Goals + +1. **GitHub star** (low friction, high visibility) +2. **PyPI install** (product usage) +3. **Discord join** (community engagement) +4. **Email signup** (for updates) +5. **Enterprise contact** (revenue) + +### 9.2 CTA Placement + +| Location | Primary CTA | Secondary CTA | +|----------|-------------|---------------| +| Hero | "Get Started" -> Install section | "View on GitHub" | +| After video | "Try it yourself" -> Install | "Join Discord" | +| Developers section | "View Docs" | "Star on GitHub" | +| Enterprise section | "Contact Sales" | "Request Demo" | +| Bottom of page | "Join Discord" | "View Documentation" | +| Sticky header (scroll) | "Get Started" | | + +### 9.3 Email Capture Strategy + +**Current**: "Register for updates" + +**Proposed**: More specific value prop +- "Get early access to new features" +- "Join [X,XXX] developers automating with AI" +- "Subscribe to the OpenAdapt newsletter (monthly, no spam)" + +--- + +## 10. Implementation Priorities + +### 10.1 Phase 1: Quick Wins (1-2 weeks) + +1. **Update hero tagline** to "Teach AI to use any software." +2. **Add "How It Works" section** with 3-step process +3. **Update differentiators** to 4-card grid (current features but better copy) +4. **Add Discord member count** to social proof +5. **Add GitHub contributors count** + +### 10.2 Phase 2: Messaging Clarity (2-4 weeks) + +1. **Add "For Developers" section** with code examples and architecture +2. **Add "For Enterprise" section** with privacy/security messaging +3. **Replace generic industry grid** with specific use case examples +4. **Add comparison table** vs. alternatives +5. **Update email signup copy** to be more specific + +### 10.3 Phase 3: Credibility Building (4-8 weeks) + +1. **Add benchmark scores** (once published) +2. **Collect and display testimonials** +3. **Create case studies** (1-2 real examples) +4. **Add logo wall** (if logos available) +5. **Add "Research" or "Publications" section** (if applicable) + +### 10.4 Phase 4: Conversion Optimization (Ongoing) + +1. **A/B test hero messaging** +2. **Track install conversion rates** +3. **Optimize CTA placement** +4. **Add video transcripts/captions for SEO** +5. **Create landing page variants** for different audiences (developer vs. enterprise) + +--- + +## Appendix A: Messaging Don'ts + +- **Don't say "AI for Desktops"** - too vague, doesn't differentiate +- **Don't say "No coding required"** - true for end users, but alienates developers +- **Don't list every industry** - pick 3-4 with real stories +- **Don't hide the CLI** - developers want to see it +- **Don't over-promise** - be honest about current capabilities + +## Appendix B: Technical Content to Add + +1. **Architecture diagram** showing package relationships +2. **Mermaid flowchart** of Record -> Train -> Deploy cycle +3. **Comparison table** of model backends (Claude, GPT, Qwen, etc.) +4. **Benchmark table** showing accuracy scores +5. **API reference link** to documentation site + +## Appendix C: SEO Keywords + +Primary: +- "GUI automation AI" +- "desktop automation AI" +- "RPA alternative AI" +- "VLM GUI agent" +- "open source computer use" + +Secondary: +- "train AI on screenshots" +- "demonstration-based automation" +- "model-agnostic automation" +- "Claude computer use alternative" +- "AI workflow automation" + +--- + +*This document is a living strategy guide. Updates should be made as OpenAdapt capabilities evolve and as user feedback is collected.* diff --git a/docs/design/openadapt-tray.md b/docs/design/openadapt-tray.md new file mode 100644 index 000000000..6e347814d --- /dev/null +++ b/docs/design/openadapt-tray.md @@ -0,0 +1,1220 @@ +# openadapt-tray Package Design + +## Overview + +`openadapt-tray` is a cross-platform system tray application that provides a graphical interface for the OpenAdapt ecosystem. It serves as a thin orchestration layer, allowing users to control recording, monitor training, view captures, and access settings without using the command line. + +## Legacy Implementation Analysis + +### Current Features (Legacy `openadapt/app/tray.py`) + +The legacy implementation uses **PySide6/Qt** for cross-platform system tray functionality: + +**Architecture:** +- `QSystemTrayIcon` for the system tray icon +- `QMenu` for context menu +- `QDialog` for configuration dialogs (replay strategy, delete confirmation) +- `pyqttoast` for toast notifications +- Multiprocessing pipes (`multiprocessing.Pipe`) for IPC with recording process +- `QThread` + `Worker` pattern for async signal handling +- Platform-specific Dock hiding on macOS via `AppKit` + +**Menu Structure:** +- Record / Stop Recording (toggle) +- Visualize submenu (lists all recordings) +- Replay submenu (lists all recordings, opens strategy dialog) +- Delete submenu (lists all recordings, confirms deletion) +- Quit + +**Key Patterns:** +- `TrackedQAction` - wraps `QAction` to send analytics events via PostHog +- Signal-based state updates (`record.starting`, `record.started`, `record.stopping`, `record.stopped`, `replay.*`) +- Toast notifications for status updates (recording started/stopped, etc.) +- Dashboard launched automatically as a background thread +- Recording process runs in a separate `multiprocessing.Process` + +**Stop Sequences:** +- Typing `oa.stop` or pressing `Ctrl` three times stops recording +- Configurable via `STOP_SEQUENCES` in config + +### Limitations of Legacy Implementation + +1. **Heavyweight dependency** - PySide6 is a large dependency (~100MB+) +2. **No global hotkeys** - Recording can only be stopped via stop sequences or tray menu +3. **Tightly coupled** - Direct imports of internal modules (crud, models, etc.) +4. **No status icons** - Same icon regardless of state +5. **No auto-start** - Manual setup required for login startup +6. **Single dashboard** - Only supports the legacy Next.js dashboard + +## New Architecture Design + +### Design Principles + +1. **Thin wrapper** - Minimal business logic; delegate to CLI or sub-packages +2. **Cross-platform first** - Consistent behavior on macOS, Windows, and Linux +3. **Lightweight** - Prefer smaller dependencies (pystray ~50KB vs PySide6 ~100MB) +4. **Event-driven** - Async status updates via IPC +5. **Configurable** - User-customizable hotkeys, icons, and behaviors + +### Package Structure + +``` +openadapt-tray/ +├── src/openadapt_tray/ +│ ├── __init__.py # Package exports, version +│ ├── __main__.py # Entry point: python -m openadapt_tray +│ ├── app.py # Main TrayApplication class +│ ├── menu.py # Menu construction and actions +│ ├── icons.py # Icon loading and status icons +│ ├── notifications.py # Cross-platform notifications +│ ├── shortcuts.py # Global hotkey handling +│ ├── config.py # Tray-specific configuration +│ ├── ipc.py # Inter-process communication +│ ├── state.py # Application state machine +│ └── platform/ +│ ├── __init__.py # Platform detection and abstraction +│ ├── base.py # Abstract base class +│ ├── macos.py # macOS-specific (AppKit, rumps optional) +│ ├── windows.py # Windows-specific (win32api) +│ └── linux.py # Linux-specific (AppIndicator) +├── assets/ +│ ├── icons/ +│ │ ├── idle.png # Default state +│ │ ├── idle@2x.png # Retina support +│ │ ├── recording.png # Recording active +│ │ ├── recording@2x.png +│ │ ├── training.png # Training in progress +│ │ ├── training@2x.png +│ │ ├── error.png # Error state +│ │ └── error@2x.png +│ └── logo.ico # Windows icon format +├── pyproject.toml +├── README.md +└── tests/ + ├── test_app.py + ├── test_menu.py + ├── test_shortcuts.py + └── test_platform.py +``` + +### Dependencies + +**Required:** +```toml +[project] +dependencies = [ + "pystray>=0.19.0", # Cross-platform system tray + "Pillow>=9.0.0", # Icon handling + "pynput>=1.7.0", # Global hotkeys + "click>=8.0.0", # CLI integration (consistent with meta-package) +] +``` + +**Optional Platform Enhancements:** +```toml +[project.optional-dependencies] +macos-native = [ + "rumps>=0.4.0", # Native macOS menu bar +] +all = [ + "openadapt-tray[macos-native]", +] +``` + +**Why pystray over PySide6/Qt:** +- Dramatically smaller (~50KB vs ~100MB) +- Pure Python, easier to install +- Sufficient for system tray use case +- Works well with pynput for hotkeys + +### Core Components + +#### 1. State Machine (`state.py`) + +```python +from enum import Enum, auto +from dataclasses import dataclass +from typing import Optional, Callable + +class TrayState(Enum): + """Application states.""" + IDLE = auto() + RECORDING_STARTING = auto() + RECORDING = auto() + RECORDING_STOPPING = auto() + TRAINING = auto() + TRAINING_PAUSED = auto() + ERROR = auto() + +@dataclass +class AppState: + """Current application state.""" + state: TrayState = TrayState.IDLE + current_capture: Optional[str] = None + training_progress: Optional[float] = None + error_message: Optional[str] = None + + def can_start_recording(self) -> bool: + return self.state == TrayState.IDLE + + def can_stop_recording(self) -> bool: + return self.state == TrayState.RECORDING + +class StateManager: + """Manages application state transitions.""" + + def __init__(self): + self._state = AppState() + self._listeners: list[Callable[[AppState], None]] = [] + + def add_listener(self, callback: Callable[[AppState], None]): + self._listeners.append(callback) + + def transition(self, new_state: TrayState, **kwargs): + """Transition to a new state and notify listeners.""" + self._state = AppState(state=new_state, **kwargs) + for listener in self._listeners: + listener(self._state) + + @property + def current(self) -> AppState: + return self._state +``` + +#### 2. Main Application (`app.py`) + +```python +import sys +import threading +from typing import Optional + +import pystray +from PIL import Image + +from openadapt_tray.state import StateManager, TrayState +from openadapt_tray.menu import MenuBuilder +from openadapt_tray.icons import IconManager +from openadapt_tray.shortcuts import HotkeyManager +from openadapt_tray.notifications import NotificationManager +from openadapt_tray.ipc import IPCClient +from openadapt_tray.config import TrayConfig +from openadapt_tray.platform import get_platform_handler + +class TrayApplication: + """Main system tray application.""" + + def __init__(self, config: Optional[TrayConfig] = None): + self.config = config or TrayConfig.load() + self.state = StateManager() + self.platform = get_platform_handler() + + # Initialize components + self.icons = IconManager() + self.notifications = NotificationManager() + self.menu_builder = MenuBuilder(self) + self.hotkeys = HotkeyManager(self.config.hotkeys) + self.ipc = IPCClient() + + # Create tray icon + self.icon = pystray.Icon( + name="openadapt", + icon=self.icons.get(TrayState.IDLE), + title="OpenAdapt", + menu=self.menu_builder.build(), + ) + + # Register state change handler + self.state.add_listener(self._on_state_change) + + # Register hotkey handlers + self._setup_hotkeys() + + def _setup_hotkeys(self): + """Configure global hotkeys.""" + self.hotkeys.register( + self.config.hotkeys.toggle_recording, + self._toggle_recording + ) + self.hotkeys.register( + self.config.hotkeys.open_dashboard, + self._open_dashboard + ) + + def _on_state_change(self, state): + """Handle state changes.""" + # Update icon + self.icon.icon = self.icons.get(state.state) + + # Update menu + self.icon.menu = self.menu_builder.build() + + # Show notification if appropriate + self._show_state_notification(state) + + def _show_state_notification(self, state): + """Show notification for state transitions.""" + messages = { + TrayState.RECORDING: ("Recording Started", f"Capturing: {state.current_capture}"), + TrayState.IDLE: ("Recording Stopped", "Capture saved"), + TrayState.TRAINING: ("Training Started", "Model training in progress"), + TrayState.ERROR: ("Error", state.error_message or "An error occurred"), + } + if state.state in messages: + title, body = messages[state.state] + self.notifications.show(title, body) + + def _toggle_recording(self): + """Toggle recording state.""" + if self.state.current.can_start_recording(): + self.start_recording() + elif self.state.current.can_stop_recording(): + self.stop_recording() + + def start_recording(self, name: Optional[str] = None): + """Start a new capture session.""" + if not self.state.current.can_start_recording(): + return + + # Prompt for name if not provided (platform-specific) + if name is None: + name = self.platform.prompt_input( + "New Recording", + "Enter a name for this capture:" + ) + if not name: + return + + self.state.transition(TrayState.RECORDING_STARTING, current_capture=name) + + # Start capture via CLI subprocess or direct API + threading.Thread( + target=self._run_capture, + args=(name,), + daemon=True + ).start() + + def _run_capture(self, name: str): + """Run capture in background thread.""" + try: + # Option 1: Via subprocess (preferred for isolation) + import subprocess + self.capture_process = subprocess.Popen( + ["openadapt", "capture", "start", "--name", name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.state.transition(TrayState.RECORDING, current_capture=name) + + except Exception as e: + self.state.transition(TrayState.ERROR, error_message=str(e)) + + def stop_recording(self): + """Stop the current capture session.""" + if not self.state.current.can_stop_recording(): + return + + self.state.transition(TrayState.RECORDING_STOPPING) + + # Send stop signal to capture process + if hasattr(self, 'capture_process') and self.capture_process: + self.capture_process.terminate() + + self.state.transition(TrayState.IDLE) + + def _open_dashboard(self): + """Open the web dashboard.""" + import webbrowser + webbrowser.open(f"http://localhost:{self.config.dashboard_port}") + + def run(self): + """Run the application.""" + # Start hotkey listener + self.hotkeys.start() + + # Platform-specific setup + self.platform.setup() + + # Run the tray icon (blocks) + self.icon.run() + + def quit(self): + """Quit the application.""" + self.hotkeys.stop() + self.ipc.close() + self.icon.stop() + +def main(): + """Entry point.""" + app = TrayApplication() + try: + app.run() + except KeyboardInterrupt: + app.quit() + +if __name__ == "__main__": + main() +``` + +#### 3. Menu Builder (`menu.py`) + +```python +from typing import TYPE_CHECKING, Callable, Optional +from functools import partial + +import pystray +from pystray import MenuItem as Item, Menu + +if TYPE_CHECKING: + from openadapt_tray.app import TrayApplication + +from openadapt_tray.state import TrayState + +class MenuBuilder: + """Builds the system tray context menu.""" + + def __init__(self, app: "TrayApplication"): + self.app = app + + def build(self) -> Menu: + """Build the current menu based on application state.""" + state = self.app.state.current + + items = [ + self._build_recording_item(state), + Menu.SEPARATOR, + self._build_captures_submenu(), + self._build_training_item(state), + Menu.SEPARATOR, + Item("Open Dashboard", self._open_dashboard), + Item("Settings...", self._open_settings), + Menu.SEPARATOR, + Item("Quit", self._quit), + ] + + return Menu(*items) + + def _build_recording_item(self, state) -> Item: + """Build record/stop recording menu item.""" + if state.state == TrayState.RECORDING: + return Item( + f"Stop Recording ({state.current_capture})", + self.app.stop_recording, + ) + elif state.state in (TrayState.RECORDING_STARTING, TrayState.RECORDING_STOPPING): + return Item( + "Recording..." if state.state == TrayState.RECORDING_STARTING else "Stopping...", + None, + enabled=False, + ) + else: + return Item( + f"Start Recording ({self.app.config.hotkeys.toggle_recording})", + self.app.start_recording, + enabled=state.can_start_recording(), + ) + + def _build_captures_submenu(self) -> Item: + """Build captures submenu.""" + captures = self._get_recent_captures() + + if not captures: + return Item( + "Recent Captures", + Menu(Item("No captures", None, enabled=False)), + ) + + capture_items = [ + Item( + f"{c.name} ({c.timestamp})", + Menu( + Item("View", partial(self._view_capture, c.path)), + Item("Delete", partial(self._delete_capture, c.path)), + ), + ) + for c in captures[:10] # Limit to 10 most recent + ] + + capture_items.append(Menu.SEPARATOR) + capture_items.append(Item("View All...", self._open_captures_list)) + + return Item("Recent Captures", Menu(*capture_items)) + + def _build_training_item(self, state) -> Item: + """Build training status/control item.""" + if state.state == TrayState.TRAINING: + progress = state.training_progress or 0 + return Item( + f"Training: {progress:.0%}", + Menu( + Item("View Progress", self._open_training_dashboard), + Item("Stop Training", self._stop_training), + ), + ) + else: + return Item( + "Training", + Menu( + Item("Start Training...", self._start_training), + Item("View Last Results", self._view_training_results), + ), + ) + + def _get_recent_captures(self): + """Get list of recent captures.""" + try: + from pathlib import Path + from openadapt_tray.config import TrayConfig + + captures_dir = Path(TrayConfig.load().captures_directory) + if not captures_dir.exists(): + return [] + + # Simple capture detection - look for capture directories + captures = [] + for d in sorted(captures_dir.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True): + if d.is_dir() and (d / "metadata.json").exists(): + from dataclasses import dataclass + from datetime import datetime + + @dataclass + class CaptureInfo: + name: str + path: str + timestamp: str + + mtime = datetime.fromtimestamp(d.stat().st_mtime) + captures.append(CaptureInfo( + name=d.name, + path=str(d), + timestamp=mtime.strftime("%Y-%m-%d %H:%M"), + )) + + return captures + except Exception: + return [] + + def _open_dashboard(self): + self.app._open_dashboard() + + def _open_settings(self): + """Open settings dialog.""" + self.app.platform.open_settings_dialog(self.app.config) + + def _quit(self): + self.app.quit() + + def _view_capture(self, path: str): + """View a capture.""" + import subprocess + subprocess.run(["openadapt", "capture", "view", path]) + + def _delete_capture(self, path: str): + """Delete a capture after confirmation.""" + if self.app.platform.confirm_dialog( + "Delete Capture", + f"Are you sure you want to delete this capture?\n{path}" + ): + import shutil + shutil.rmtree(path) + self.app.notifications.show("Capture Deleted", "The capture has been removed.") + + def _open_captures_list(self): + """Open captures list in dashboard.""" + import webbrowser + webbrowser.open(f"http://localhost:{self.app.config.dashboard_port}/captures") + + def _open_training_dashboard(self): + """Open training dashboard.""" + import webbrowser + webbrowser.open(f"http://localhost:{self.app.config.dashboard_port}/training") + + def _start_training(self): + """Open training configuration dialog.""" + # This would open a dialog to select capture and model + self.app.platform.open_training_dialog() + + def _stop_training(self): + """Stop current training.""" + import subprocess + subprocess.run(["openadapt", "train", "stop"]) + self.app.state.transition(TrayState.IDLE) + + def _view_training_results(self): + """View last training results.""" + import subprocess + subprocess.run(["openadapt", "train", "status"]) +``` + +#### 4. Global Hotkeys (`shortcuts.py`) + +```python +from dataclasses import dataclass +from typing import Callable, Dict, Optional +import threading + +from pynput import keyboard + +@dataclass +class HotkeyConfig: + """Hotkey configuration.""" + toggle_recording: str = "++r" + open_dashboard: str = "++d" + stop_recording: str = "++" # Triple ctrl (legacy compat) + +class HotkeyManager: + """Manages global hotkeys.""" + + def __init__(self, config: Optional[HotkeyConfig] = None): + self.config = config or HotkeyConfig() + self._handlers: Dict[str, Callable] = {} + self._listener: Optional[keyboard.GlobalHotKeys] = None + self._ctrl_count = 0 + self._ctrl_timer: Optional[threading.Timer] = None + + def register(self, hotkey: str, handler: Callable): + """Register a hotkey handler.""" + self._handlers[hotkey] = handler + + def start(self): + """Start listening for hotkeys.""" + # Build hotkey dict for pynput + hotkeys = {} + for combo, handler in self._handlers.items(): + if combo == "++": + # Special handling for triple-ctrl + continue + hotkeys[combo] = handler + + self._listener = keyboard.GlobalHotKeys(hotkeys) + self._listener.start() + + # Also listen for triple-ctrl pattern + if "++" in self._handlers: + self._start_ctrl_listener() + + def _start_ctrl_listener(self): + """Start listener for triple-ctrl pattern.""" + def on_press(key): + if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r: + self._on_ctrl_press() + + def on_release(key): + pass + + self._key_listener = keyboard.Listener( + on_press=on_press, + on_release=on_release, + ) + self._key_listener.start() + + def _on_ctrl_press(self): + """Handle ctrl key press for triple-ctrl detection.""" + self._ctrl_count += 1 + + # Reset timer + if self._ctrl_timer: + self._ctrl_timer.cancel() + + if self._ctrl_count >= 3: + self._ctrl_count = 0 + handler = self._handlers.get("++") + if handler: + handler() + else: + # Reset count after 500ms + self._ctrl_timer = threading.Timer(0.5, self._reset_ctrl_count) + self._ctrl_timer.start() + + def _reset_ctrl_count(self): + self._ctrl_count = 0 + + def stop(self): + """Stop listening for hotkeys.""" + if self._listener: + self._listener.stop() + if hasattr(self, '_key_listener'): + self._key_listener.stop() + if self._ctrl_timer: + self._ctrl_timer.cancel() +``` + +#### 5. Platform Abstraction (`platform/`) + +**Base class (`platform/base.py`):** + +```python +from abc import ABC, abstractmethod +from typing import Optional + +class PlatformHandler(ABC): + """Abstract base class for platform-specific functionality.""" + + @abstractmethod + def setup(self): + """Platform-specific setup.""" + pass + + @abstractmethod + def prompt_input(self, title: str, message: str) -> Optional[str]: + """Show input dialog and return user input.""" + pass + + @abstractmethod + def confirm_dialog(self, title: str, message: str) -> bool: + """Show confirmation dialog and return result.""" + pass + + @abstractmethod + def open_settings_dialog(self, config): + """Open settings dialog.""" + pass + + @abstractmethod + def open_training_dialog(self): + """Open training configuration dialog.""" + pass + + def setup_autostart(self, enabled: bool): + """Configure auto-start on login.""" + pass +``` + +**macOS implementation (`platform/macos.py`):** + +```python +import subprocess +from typing import Optional + +from .base import PlatformHandler + +class MacOSHandler(PlatformHandler): + """macOS-specific functionality.""" + + def setup(self): + """Hide from Dock, show only in menu bar.""" + try: + from AppKit import NSApplication, NSApplicationActivationPolicyAccessory + NSApplication.sharedApplication().setActivationPolicy_( + NSApplicationActivationPolicyAccessory + ) + except ImportError: + pass # AppKit not available + + def prompt_input(self, title: str, message: str) -> Optional[str]: + """Show native macOS input dialog.""" + script = f''' + tell application "System Events" + display dialog "{message}" default answer "" with title "{title}" + return text returned of result + end tell + ''' + try: + result = subprocess.run( + ["osascript", "-e", script], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return result.stdout.strip() + except Exception: + pass + return None + + def confirm_dialog(self, title: str, message: str) -> bool: + """Show native macOS confirmation dialog.""" + script = f''' + tell application "System Events" + display dialog "{message}" with title "{title}" buttons {{"Cancel", "OK"}} default button "OK" + return button returned of result + end tell + ''' + try: + result = subprocess.run( + ["osascript", "-e", script], + capture_output=True, + text=True, + ) + return result.returncode == 0 and "OK" in result.stdout + except Exception: + return False + + def open_settings_dialog(self, config): + """Open settings in default browser.""" + import webbrowser + webbrowser.open(f"http://localhost:{config.dashboard_port}/settings") + + def open_training_dialog(self): + """Open training dialog in browser.""" + import webbrowser + webbrowser.open("http://localhost:8080/training/new") + + def setup_autostart(self, enabled: bool): + """Configure Launch Agent for auto-start.""" + import os + from pathlib import Path + + plist_path = Path.home() / "Library/LaunchAgents/ai.openadapt.tray.plist" + + if enabled: + plist_content = ''' + + + + Label + ai.openadapt.tray + ProgramArguments + + /usr/local/bin/openadapt-tray + + RunAtLoad + + KeepAlive + + +''' + plist_path.parent.mkdir(parents=True, exist_ok=True) + plist_path.write_text(plist_content) + subprocess.run(["launchctl", "load", str(plist_path)]) + else: + if plist_path.exists(): + subprocess.run(["launchctl", "unload", str(plist_path)]) + plist_path.unlink() +``` + +**Windows implementation (`platform/windows.py`):** + +```python +import ctypes +from typing import Optional + +from .base import PlatformHandler + +class WindowsHandler(PlatformHandler): + """Windows-specific functionality.""" + + def setup(self): + """Windows-specific setup.""" + pass # No special setup needed + + def prompt_input(self, title: str, message: str) -> Optional[str]: + """Show Windows input dialog using ctypes.""" + try: + import tkinter as tk + from tkinter import simpledialog + + root = tk.Tk() + root.withdraw() + result = simpledialog.askstring(title, message) + root.destroy() + return result + except Exception: + return None + + def confirm_dialog(self, title: str, message: str) -> bool: + """Show Windows confirmation dialog.""" + MB_OKCANCEL = 0x01 + MB_ICONQUESTION = 0x20 + IDOK = 1 + + result = ctypes.windll.user32.MessageBoxW( + 0, message, title, MB_OKCANCEL | MB_ICONQUESTION + ) + return result == IDOK + + def open_settings_dialog(self, config): + import webbrowser + webbrowser.open(f"http://localhost:{config.dashboard_port}/settings") + + def open_training_dialog(self): + import webbrowser + webbrowser.open("http://localhost:8080/training/new") + + def setup_autostart(self, enabled: bool): + """Configure Windows Registry for auto-start.""" + import winreg + + key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" + app_name = "OpenAdapt" + + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_ALL_ACCESS) + + if enabled: + import sys + exe_path = sys.executable.replace("python.exe", "Scripts\\openadapt-tray.exe") + winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, exe_path) + else: + try: + winreg.DeleteValue(key, app_name) + except FileNotFoundError: + pass + + winreg.CloseKey(key) + except Exception: + pass +``` + +#### 6. Configuration (`config.py`) + +```python +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional +import json + +from openadapt_tray.shortcuts import HotkeyConfig + +@dataclass +class TrayConfig: + """Tray application configuration.""" + + # Hotkeys + hotkeys: HotkeyConfig = field(default_factory=HotkeyConfig) + + # Paths + captures_directory: str = "~/openadapt/captures" + training_output_directory: str = "~/openadapt/training" + + # Dashboard + dashboard_port: int = 8080 + auto_launch_dashboard: bool = True + + # Behavior + auto_start_on_login: bool = False + minimize_to_tray: bool = True + show_notifications: bool = True + notification_duration_ms: int = 5000 + + # Recording + default_record_audio: bool = True + default_transcribe: bool = True + stop_on_triple_ctrl: bool = True + + # Appearance + use_native_dialogs: bool = True + + @classmethod + def config_path(cls) -> Path: + """Get configuration file path.""" + return Path.home() / ".config" / "openadapt" / "tray.json" + + @classmethod + def load(cls) -> "TrayConfig": + """Load configuration from file.""" + path = cls.config_path() + if path.exists(): + try: + data = json.loads(path.read_text()) + hotkeys_data = data.pop("hotkeys", {}) + return cls( + hotkeys=HotkeyConfig(**hotkeys_data), + **data + ) + except Exception: + pass + return cls() + + def save(self): + """Save configuration to file.""" + path = self.config_path() + path.parent.mkdir(parents=True, exist_ok=True) + + data = { + "hotkeys": { + "toggle_recording": self.hotkeys.toggle_recording, + "open_dashboard": self.hotkeys.open_dashboard, + "stop_recording": self.hotkeys.stop_recording, + }, + "captures_directory": self.captures_directory, + "training_output_directory": self.training_output_directory, + "dashboard_port": self.dashboard_port, + "auto_launch_dashboard": self.auto_launch_dashboard, + "auto_start_on_login": self.auto_start_on_login, + "minimize_to_tray": self.minimize_to_tray, + "show_notifications": self.show_notifications, + "notification_duration_ms": self.notification_duration_ms, + "default_record_audio": self.default_record_audio, + "default_transcribe": self.default_transcribe, + "stop_on_triple_ctrl": self.stop_on_triple_ctrl, + "use_native_dialogs": self.use_native_dialogs, + } + + path.write_text(json.dumps(data, indent=2)) +``` + +#### 7. Notifications (`notifications.py`) + +```python +import sys +from typing import Optional + +class NotificationManager: + """Cross-platform notification manager.""" + + def __init__(self): + self._backend = self._detect_backend() + + def _detect_backend(self) -> str: + """Detect best notification backend for platform.""" + if sys.platform == "darwin": + return "macos" + elif sys.platform == "win32": + return "windows" + else: + return "linux" + + def show( + self, + title: str, + body: str, + icon_path: Optional[str] = None, + duration_ms: int = 5000, + ): + """Show a notification.""" + if self._backend == "macos": + self._show_macos(title, body) + elif self._backend == "windows": + self._show_windows(title, body, icon_path, duration_ms) + else: + self._show_linux(title, body, icon_path) + + def _show_macos(self, title: str, body: str): + """Show notification on macOS.""" + import subprocess + script = f''' + display notification "{body}" with title "{title}" + ''' + subprocess.run(["osascript", "-e", script], capture_output=True) + + def _show_windows(self, title: str, body: str, icon_path: Optional[str], duration_ms: int): + """Show notification on Windows using pystray's built-in notify.""" + # pystray handles this via icon.notify() + pass + + def _show_linux(self, title: str, body: str, icon_path: Optional[str]): + """Show notification on Linux.""" + try: + import subprocess + cmd = ["notify-send", title, body] + if icon_path: + cmd.extend(["-i", icon_path]) + subprocess.run(cmd, capture_output=True) + except Exception: + pass +``` + +### pyproject.toml + +```toml +[project] +name = "openadapt-tray" +version = "0.1.0" +description = "System tray application for OpenAdapt" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +authors = [ + {name = "MLDSAI Inc.", email = "richard@mldsai.com"} +] +keywords = ["gui", "system-tray", "menu-bar", "openadapt"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "pystray>=0.19.0", + "Pillow>=9.0.0", + "pynput>=1.7.0", + "click>=8.0.0", +] + +[project.optional-dependencies] +macos-native = [ + "rumps>=0.4.0", + "pyobjc-framework-Cocoa>=9.0", +] +dev = [ + "pytest>=8.0.0", + "pytest-mock>=3.10.0", + "ruff>=0.1.0", +] +all = [ + "openadapt-tray[macos-native]", +] + +[project.scripts] +openadapt-tray = "openadapt_tray.app:main" + +[project.gui-scripts] +openadapt-tray-gui = "openadapt_tray.app:main" + +[project.urls] +Homepage = "https://openadapt.ai" +Documentation = "https://docs.openadapt.ai" +Repository = "https://github.com/OpenAdaptAI/openadapt-tray" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/openadapt_tray"] + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.pytest.ini_options] +testpaths = ["tests"] +``` + +## User Experience + +### First-Run Experience + +1. **Installation**: `pip install openadapt-tray` +2. **Launch**: `openadapt-tray` or via Applications menu +3. **First Run Dialog** (if no config exists): + - Welcome message + - Option to configure hotkeys + - Option to enable auto-start + - Link to documentation +4. **Tray Icon**: Appears in system tray/menu bar +5. **Dashboard**: Auto-opens (configurable) + +### Menu Structure + +``` +[OpenAdapt Tray Icon] +├── Start Recording (Ctrl+Shift+R) +│ └── [When recording: "Stop Recording (task-name)"] +├── ───────────── +├── Recent Captures +│ ├── login-flow (2024-01-15 14:30) +│ │ ├── View +│ │ └── Delete +│ ├── checkout (2024-01-15 10:15) +│ │ ├── View +│ │ └── Delete +│ ├── ... (up to 10 items) +│ ├── ───────────── +│ └── View All... +├── Training +│ ├── Start Training... +│ └── View Last Results +│ └── [When training: "Training: 45% | View Progress | Stop"] +├── ───────────── +├── Open Dashboard (Ctrl+Shift+D) +├── Settings... +├── ───────────── +└── Quit +``` + +### Status Icons + +| State | Icon Description | Color | +|-------|------------------|-------| +| Idle | OpenAdapt logo | Blue/Gray | +| Recording | Pulsing red dot overlay | Red | +| Recording Starting | Spinning indicator | Yellow | +| Training | Gear icon | Purple | +| Error | Exclamation mark | Red | + +### Keyboard Shortcuts + +| Action | Default Shortcut | Configurable | +|--------|------------------|--------------| +| Toggle Recording | `Ctrl+Shift+R` | Yes | +| Open Dashboard | `Ctrl+Shift+D` | Yes | +| Stop Recording | `Ctrl Ctrl Ctrl` (triple tap) | Yes | + +### Notifications + +| Event | Title | Body | +|-------|-------|------| +| Recording Started | "Recording Started" | "Capturing: {task-name}" | +| Recording Stopped | "Recording Stopped" | "Capture saved" | +| Training Started | "Training Started" | "Model training in progress" | +| Training Complete | "Training Complete" | "Model saved to {path}" | +| Error | "Error" | "{error message}" | + +## Integration with Ecosystem + +### CLI Integration + +The tray app delegates to the `openadapt` CLI for all operations: + +```python +# Starting a capture +subprocess.Popen(["openadapt", "capture", "start", "--name", name]) + +# Stopping a capture +subprocess.Popen(["openadapt", "capture", "stop"]) + +# Starting training +subprocess.Popen(["openadapt", "train", "start", "--capture", capture_path]) + +# Checking training status +result = subprocess.run(["openadapt", "train", "status"], capture_output=True) +``` + +### Direct API Integration (Alternative) + +For tighter integration, the tray can import sub-packages directly: + +```python +try: + from openadapt_capture import CaptureSession + + session = CaptureSession(name=name, record_audio=True) + session.start() +except ImportError: + # Fall back to CLI + subprocess.Popen(["openadapt", "capture", "start", "--name", name]) +``` + +### Dashboard Integration + +- Auto-launches the dashboard web server on startup (configurable) +- "Open Dashboard" opens browser to `http://localhost:8080` +- Settings page accessible via tray menu + +## Future Enhancements + +1. **Native macOS app** using `rumps` for a more native feel +2. **Electron wrapper** for consistent cross-platform UI +3. **Recording preview** - show recent screenshot in menu +4. **Quick actions** - right-click for immediate actions +5. **Status bar text** - show recording duration on macOS +6. **Multi-monitor support** - select which monitor to record +7. **Cloud sync** - sync captures and settings across devices +8. **Plugin system** - allow third-party menu extensions + +## Migration from Legacy + +### Compatibility + +The new tray app maintains backward compatibility with: +- Legacy stop sequences (`oa.stop`, triple-ctrl) +- PostHog analytics events +- Configuration file locations + +### Migration Path + +1. Install `openadapt-tray` alongside legacy +2. Both can coexist (different process names) +3. Legacy can be deprecated when new tray is stable +4. Configuration migration script provided + +--- + +*This design enables a lightweight, cross-platform system tray experience while maintaining integration with the OpenAdapt ecosystem's CLI-first architecture.* diff --git a/docs/design/repo-rename-analysis.md b/docs/design/repo-rename-analysis.md new file mode 100644 index 000000000..e66dac023 --- /dev/null +++ b/docs/design/repo-rename-analysis.md @@ -0,0 +1,286 @@ +# Repository Rename Analysis: OpenAdapt to openadapt + +**Date:** January 2026 +**Status:** Decision Document +**Author:** Engineering Team + +--- + +## Executive Summary + +This document analyzes whether to rename the main OpenAdapt GitHub repository from `OpenAdapt` (mixed case) to `openadapt` (lowercase) to align with Python conventions and existing sub-packages. + +**Recommendation: DO NOT RENAME at this time.** + +The costs and risks of renaming outweigh the benefits. The minor consistency improvement does not justify the potential for broken links, documentation updates, and brand dilution. + +--- + +## Current State + +| Component | Current Name | Case | +|-----------|-------------|------| +| **Main Repository** | `OpenAdaptAI/OpenAdapt` | Mixed | +| **GitHub Organization** | `OpenAdaptAI` | Mixed | +| **Sub-packages** | `openadapt-ml`, `openadapt-capture`, etc. | Lowercase | +| **PyPI Package** | `openadapt` | Lowercase | +| **Python Imports** | `import openadapt` | Lowercase | +| **pyproject.toml Repository URL** | Already points to `openadapt` (lowercase) | Lowercase | + +**Key Observation:** The `pyproject.toml` already uses lowercase in the Repository URL: +```toml +Repository = "https://github.com/OpenAdaptAI/openadapt" +``` + +This suggests the team anticipated or intended lowercase naming, but GitHub currently shows `OpenAdapt`. + +--- + +## Industry Research: How Major Python Projects Handle Repository Naming + +| Project | Organization | Repository | PyPI Package | Notes | +|---------|-------------|------------|--------------|-------| +| **LangChain** | `langchain-ai` | `langchain` | `langchain` | All lowercase | +| **PyTorch** | `pytorch` | `pytorch` | `torch` | All lowercase | +| **TensorFlow** | `tensorflow` | `tensorflow` | `tensorflow` | All lowercase | +| **Hugging Face** | `huggingface` | `transformers` | `transformers` | All lowercase | +| **FastAPI** | `tiangolo` | `fastapi` | `fastapi` | All lowercase | +| **scikit-learn** | `scikit-learn` | `scikit-learn` | `scikit-learn` | All lowercase with hyphen | + +**Conclusion:** The overwhelming convention in Python open-source projects is **all lowercase** for repository names. + +--- + +## GitHub Redirect Behavior + +Based on [GitHub's documentation](https://docs.github.com/en/repositories/creating-and-managing-repositories/renaming-a-repository): + +### What Gets Redirected (Indefinitely) +- Web traffic to the old URL +- `git clone`, `git fetch`, `git push` operations +- Issues, wikis, stars, followers + +### What Breaks Immediately +- **GitHub Actions** referencing the repository by name will fail with "repository not found" +- **GitHub Pages** custom domain URLs are not automatically redirected + +### Redirect Persistence +- Redirects persist **indefinitely** unless: + 1. A new repository is created with the old name + 2. GitHub support is asked to remove them + +### Important Warning +From [GitHub Community discussions](https://github.com/orgs/community/discussions/22669): "If you create a new repository under your account in the future, do not reuse the original name of the renamed repository. If you do, redirects to the renamed repository will no longer work." + +--- + +## Detailed Analysis + +### Arguments FOR Renaming to Lowercase + +| Argument | Weight | Rationale | +|----------|--------|-----------| +| **Consistency with sub-packages** | Medium | All sub-packages use lowercase (`openadapt-ml`, `openadapt-capture`, etc.) | +| **Python convention** | Medium | Standard practice in Python ecosystem (see industry research) | +| **PyPI alignment** | Medium | Package name is `openadapt` (lowercase) | +| **Import alignment** | Low | `import openadapt` works regardless of repo name | +| **URL simplicity** | Low | `github.com/OpenAdaptAI/openadapt` slightly cleaner | +| **Already in pyproject.toml** | High | Repository URL already shows lowercase intent | + +### Arguments AGAINST Renaming + +| Argument | Weight | Rationale | +|----------|--------|-----------| +| **Brand recognition** | High | "OpenAdapt" as two words (Open + Adapt) reinforces brand identity | +| **Breaking changes risk** | High | External links, bookmarks, documentation, blog posts, academic citations | +| **GitHub org inconsistency** | Medium | Organization is `OpenAdaptAI` (mixed case) - renaming repo creates inconsistency | +| **Documentation updates** | Medium | 1,343 occurrences of "OpenAdapt" across 78 files need review | +| **SEO impact** | Medium | Existing search rankings tied to "OpenAdapt" | +| **Minimal actual benefit** | High | GitHub URLs are case-insensitive for access purposes | +| **Legacy code references** | Medium | Legacy directory has extensive "OpenAdapt" references | + +--- + +## Technical Impact Assessment + +### Files Requiring Updates if Renamed + +Based on codebase analysis: + +| Category | File Count | Occurrences | Update Required? | +|----------|------------|-------------|------------------| +| Documentation (*.md) | 37 | ~200+ | Review each | +| GitHub workflows (*.yml) | 10 | ~50+ | Critical review | +| Python source files | 15 | ~50+ | Review imports | +| Configuration files | 5 | ~20+ | Review URLs | +| Legacy code | 20+ | ~900+ | May leave as-is | + +### CI/CD Impact + +Current workflows use relative paths and don't hard-code the repository name, so **minimal CI/CD impact expected**. + +However, any external workflows or actions referencing `OpenAdaptAI/OpenAdapt` would need updates. + +### Impact on Forks and Clones + +- **Existing clones:** Continue working via redirects, but should update with `git remote set-url` +- **Existing forks:** Maintain their existing names and remotes +- **New forks:** Would fork from the new lowercase name + +--- + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Broken external links | Medium | Medium | GitHub redirects handle most cases | +| Academic citation issues | Low | Medium | Papers cite DOIs or specific versions | +| SEO ranking drop | Low | Low | Temporary if any; redirects preserve link equity | +| User confusion | Medium | Low | Clear communication and documentation | +| GitHub Actions failures | Low | High | Audit and update before rename | +| Brand dilution | Medium | Medium | None - cannot mitigate if lowercase chosen | + +--- + +## Alternative Approaches + +### Option A: Do Nothing (RECOMMENDED) +- Keep repository as `OpenAdapt` +- Accept minor inconsistency with sub-packages +- No risk, no disruption + +### Option B: Rename to Lowercase +- Change repository to `openadapt` +- Update documentation +- Communicate to users +- Accept brand/visual trade-off + +### Option C: Rename Organization and Repository +- Change `OpenAdaptAI` to `openadaptai` +- Change `OpenAdapt` to `openadapt` +- Complete consistency, but much higher disruption +- **NOT RECOMMENDED** - organization rename is significantly more disruptive + +### Option D: Create Alias via Transfer +- Transfer repository to a new `openadapt` repo +- Keep `OpenAdapt` as a redirect-only stub +- **NOT RECOMMENDED** - unnecessarily complex + +--- + +## Recommendation + +**Recommendation: Do Not Rename (Option A)** + +### Rationale + +1. **GitHub URLs are case-insensitive** - Users can access via `github.com/OpenAdaptAI/openadapt` or `github.com/openadaptai/OpenAdapt` interchangeably + +2. **Brand value** - "OpenAdapt" with capitalization clearly shows the "Open" + "Adapt" word composition, which is meaningful for the project's identity + +3. **Risk/benefit ratio** - The benefits are cosmetic while the risks (broken links, confusion, documentation churn) are concrete + +4. **Organization inconsistency** - Renaming only the repo while keeping `OpenAdaptAI` creates a new inconsistency + +5. **Industry examples** - While most Python projects use lowercase, several successful projects (like early versions of major projects) maintained mixed-case names without issue + +6. **pyproject.toml already lowercase** - The `Repository` URL in `pyproject.toml` already shows lowercase, providing implicit consistency for programmatic access + +--- + +## If Renaming is Chosen: Migration Plan + +Should the decision be made to rename despite the recommendation, here is the migration plan: + +### Phase 1: Preparation (1 week before) +1. Audit all GitHub Actions and CI/CD workflows +2. Document all external references (blog posts, papers, etc.) +3. Prepare communication for Discord and mailing lists +4. Create redirect documentation + +### Phase 2: Execution (Day of) +1. Perform the rename via GitHub Settings +2. Update `pyproject.toml` repository URL (if needed) +3. Update README.md badge URLs +4. Push updated documentation + +### Phase 3: Communication (Day of + 1 week) +1. Announce on Discord +2. Post on social media +3. Email contributors +4. Update any linked resources + +### Phase 4: Follow-up (1 month) +1. Monitor for broken links +2. Update external documentation (readthedocs, etc.) +3. Check Google Search Console for indexing issues + +--- + +## Timeline + +| Milestone | Date | Notes | +|-----------|------|-------| +| Decision | TBD | Pending team discussion | +| If renaming: Preparation | T+0 to T+7 days | Audit and documentation | +| If renaming: Execution | T+7 days | Actual rename | +| If renaming: Stabilization | T+7 to T+30 days | Monitor and fix issues | + +--- + +## Conclusion + +While lowercase repository naming is the Python convention and would create better consistency with sub-packages, the **costs outweigh the benefits** for the main OpenAdapt repository. The recommendation is to **keep the current `OpenAdapt` naming** for the following key reasons: + +1. Brand recognition and identity +2. Risk of breaking external references +3. GitHub URLs are case-insensitive anyway +4. Organization name would remain inconsistent regardless +5. The `pyproject.toml` already uses lowercase, providing programmatic consistency + +If consistency is deemed critical in the future, consider renaming the organization and all repositories together as a single coordinated effort, rather than piecemeal changes. + +--- + +## References + +- [GitHub: Renaming a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/renaming-a-repository) +- [GitHub Community: How long does GitHub forward renamed repos?](https://github.com/orgs/community/discussions/22669) +- [GitHub Community: Duration of Web Traffic Redirection](https://github.com/orgs/community/discussions/110367) +- [LangChain GitHub](https://github.com/langchain-ai/langchain) +- [Hugging Face Transformers](https://github.com/huggingface/transformers) + +--- + +## Appendix A: Files Containing "OpenAdapt" References + +Key files with the highest occurrence counts: + +| File | Count | Notes | +|------|-------|-------| +| `legacy/CHANGELOG.md` | 911 | Historical, may leave unchanged | +| `README.md` | 21 | Brand mentions, badges | +| `docs/contributing.md` | 18 | Contribution guidelines | +| `legacy/build.py` | 19 | Build scripts | +| `docs/design/landing-page-strategy.md` | 20 | Strategy document | +| `docs/architecture-evolution.md` | 14 | Architecture docs | + +Total: **1,343 occurrences across 78 files** + +--- + +## Appendix B: Sub-package Repository Naming + +All sub-packages follow lowercase convention: + +| Repository | PyPI Package | +|------------|--------------| +| `openadapt-capture` | `openadapt-capture` | +| `openadapt-ml` | `openadapt-ml` | +| `openadapt-evals` | `openadapt-evals` | +| `openadapt-viewer` | `openadapt-viewer` | +| `openadapt-grounding` | `openadapt-grounding` | +| `openadapt-retrieval` | `openadapt-retrieval` | +| `openadapt-privacy` | `openadapt-privacy` | + +This consistency is desirable but not critical enough to justify renaming the main repository. diff --git a/docs/design/telemetry-design.md b/docs/design/telemetry-design.md new file mode 100644 index 000000000..cd0ecc343 --- /dev/null +++ b/docs/design/telemetry-design.md @@ -0,0 +1,895 @@ +# Telemetry Design for OpenAdapt Packages + +## Overview + +This document outlines the design for adding optional telemetry to all OpenAdapt packages. The system is designed to be: + +- **Opt-in by default** (or easily disabled) +- **Privacy-respecting** (no PII, no screenshots, minimal data) +- **Developer-aware** (internal usage tagged for filtering) +- **Unified** (shared module across all packages) + +## Table of Contents + +1. [Service Recommendation](#service-recommendation) +2. [Architecture](#architecture) +3. [Implementation Approach](#implementation-approach) +4. [Configuration Options](#configuration-options) +5. [Privacy Considerations](#privacy-considerations) +6. [Internal Usage Tagging](#internal-usage-tagging) +7. [Code Examples](#code-examples) +8. [Migration Plan](#migration-plan) +9. [References](#references) + +--- + +## Service Recommendation + +### Recommendation: GlitchTip (Self-Hosted) + Sentry SDK + +After evaluating both options, we recommend **continuing with GlitchTip** (already in use in the legacy codebase) with the Sentry Python SDK. + +### Comparison + +| Feature | GlitchTip | Sentry | +|---------|-----------|--------| +| **Pricing** | Free (self-hosted) or $15/mo (100K errors) | Free tier limited, paid plans start higher | +| **Self-Hosting** | Simple (4 components: backend, workers, Redis, PostgreSQL) | Complex (12+ components including Kafka, Zookeeper, ClickHouse) | +| **Resource Requirements** | Minimal (1GB RAM, 1 CPU core) | Heavy (requires significant infrastructure) | +| **SDK Compatibility** | Uses Sentry SDK (drop-in compatible) | Native SDK | +| **Open Source** | Fully open source | Partially open source | +| **Features** | Error tracking, uptime monitoring, basic performance | Full APM, session replay, distributed tracing | +| **Privacy** | Self-hosted = full data control | Cloud = data sent to Sentry servers | + +### Rationale + +1. **Existing Integration**: The legacy OpenAdapt codebase already uses GlitchTip (DSN: `app.glitchtip.com`) +2. **Privacy-First**: Self-hosting ensures complete control over sensitive automation data +3. **Cost-Effective**: Free for self-hosted or very affordable cloud option +4. **SDK Compatibility**: Uses the battle-tested Sentry Python SDK +5. **Simplicity**: Easier to deploy and maintain than self-hosted Sentry +6. **Open Source Alignment**: Matches OpenAdapt's open-source philosophy + +### GlitchTip Cloud vs Self-Hosted + +| Option | Pros | Cons | +|--------|------|------| +| **Cloud (glitchtip.com)** | Zero maintenance, instant setup | Monthly cost, data leaves your infrastructure | +| **Self-Hosted** | Free, full data control, customizable | Requires server, maintenance overhead | + +**Recommendation**: Start with GlitchTip Cloud for simplicity, migrate to self-hosted if needed. + +--- + +## Architecture + +### Shared Telemetry Module + +We propose a new package `openadapt-telemetry` that provides a unified telemetry interface for all OpenAdapt packages. + +``` +openadapt-telemetry/ +├── src/openadapt_telemetry/ +│ ├── __init__.py # Public API exports +│ ├── config.py # Configuration management +│ ├── client.py # Telemetry client (Sentry wrapper) +│ ├── events.py # Event types and helpers +│ ├── privacy.py # PII filtering and scrubbing +│ └── decorators.py # Convenience decorators +└── pyproject.toml +``` + +### Package Integration + +```mermaid +graph TD + subgraph Packages["OpenAdapt Packages"] + CAP[openadapt-capture] + ML[openadapt-ml] + EVL[openadapt-evals] + VWR[openadapt-viewer] + GRD[openadapt-grounding] + RET[openadapt-retrieval] + PRV[openadapt-privacy] + end + + subgraph Telemetry["Telemetry Layer"] + TEL[openadapt-telemetry] + CONFIG[Config Manager] + FILTER[Privacy Filter] + end + + subgraph Backend["Backend"] + GT[GlitchTip] + end + + CAP --> TEL + ML --> TEL + EVL --> TEL + VWR --> TEL + GRD --> TEL + RET --> TEL + PRV --> TEL + + TEL --> CONFIG + TEL --> FILTER + TEL --> GT +``` + +--- + +## Implementation Approach + +### Option A: Shared Package (Recommended) + +Create `openadapt-telemetry` as a dependency for all packages. + +**Pros:** +- Single source of truth for telemetry logic +- Consistent behavior across all packages +- Easy to update and maintain +- Centralized privacy controls + +**Cons:** +- Additional dependency +- Version coordination required + +### Option B: Per-Package Implementation + +Each package implements its own telemetry. + +**Pros:** +- Package independence +- No cross-package dependencies + +**Cons:** +- Code duplication +- Inconsistent implementations +- Harder to maintain privacy controls + +### Decision: Option A (Shared Package) + +The shared package approach aligns with the meta-package architecture and ensures consistency. + +--- + +## Configuration Options + +### Environment Variables + +```bash +# Primary opt-out mechanism (industry standard) +OPENADAPT_TELEMETRY_ENABLED=false # Disable all telemetry +DO_NOT_TRACK=1 # Universal opt-out (alternative) + +# Internal/developer mode +OPENADAPT_INTERNAL=true # Tag as internal usage +OPENADAPT_DEV=true # Development mode (alternative) + +# Configuration overrides +OPENADAPT_TELEMETRY_DSN= # Custom DSN +OPENADAPT_TELEMETRY_ENVIRONMENT=dev # Environment name +OPENADAPT_TELEMETRY_SAMPLE_RATE=0.1 # Sampling rate (0.0-1.0) +``` + +### Configuration File + +```json +// ~/.config/openadapt/telemetry.json +{ + "enabled": true, + "internal": false, + "dsn": null, + "environment": "production", + "sample_rate": 1.0, + "error_tracking": true, + "performance_tracking": false, + "feature_usage": true +} +``` + +### Priority Order + +1. Environment variables (highest priority) +2. Configuration file +3. Package defaults (lowest priority) + +### Default Configuration + +```python +DEFAULTS = { + "enabled": True, # Enabled by default, easy opt-out + "internal": False, # External user by default + "dsn": "https://xxx@app.glitchtip.com/XXXX", + "environment": "production", + "sample_rate": 1.0, # 100% for errors + "traces_sample_rate": 0.01, # 1% for performance + "error_tracking": True, + "performance_tracking": True, + "feature_usage": True, + "send_default_pii": False, # Never send PII by default +} +``` + +--- + +## Privacy Considerations + +### What We Collect (Ethical Data) + +| Category | Data Collected | Purpose | +|----------|---------------|---------| +| **Error Tracking** | Exception type, stack trace, error message | Bug fixing, stability monitoring | +| **Performance** | Function timing, memory usage | Optimization, bottleneck detection | +| **Feature Usage** | Feature names, operation counts | Prioritize development, understand needs | +| **Environment** | OS, Python version, package versions | Compatibility testing, support | +| **Session** | Anonymous session ID, duration | Usage patterns, engagement | + +### What We Never Collect + +| Category | Data NOT Collected | Reason | +|----------|-------------------|--------| +| **PII** | Names, emails, IP addresses | Privacy violation | +| **Screenshots** | Screen captures, images | Highly sensitive | +| **User Content** | Text typed, file contents | Privacy violation | +| **Credentials** | API keys, passwords, tokens | Security risk | +| **File Paths** | Full paths (especially with usernames) | PII leakage | +| **Network Data** | URLs, request bodies | Sensitive information | +| **Biometrics** | Mouse patterns, typing cadence | Privacy violation | + +### PII Scrubbing + +```python +# Automatically scrubbed from all events +PII_DENYLIST = [ + "password", + "secret", + "token", + "api_key", + "authorization", + "cookie", + "session", + "email", + "phone", + "address", + "ssn", + "credit_card", +] + +# Path sanitization +def sanitize_path(path: str) -> str: + """Remove username from file paths.""" + # /Users/john/code/file.py -> /Users//code/file.py + return re.sub(r'/Users/[^/]+/', '/Users//', path) +``` + +### GDPR Compliance + +1. **Consent**: Telemetry is opt-in or easily disabled +2. **Data Minimization**: Collect only necessary data +3. **Purpose Limitation**: Use only for stated purposes +4. **Transparency**: Document what is collected +5. **Right to Erasure**: Provide way to request data deletion +6. **Data Protection**: Self-hosted option for full control + +--- + +## Internal Usage Tagging + +### Tagging Strategy + +Internal OpenAdapt developers and testers should be tagged so their usage can be filtered out when analyzing real user behavior. + +### Detection Methods + +```python +def is_internal_user() -> bool: + """Determine if current usage is from internal team.""" + + # Method 1: Explicit environment variable + if os.getenv("OPENADAPT_INTERNAL", "").lower() in ("true", "1", "yes"): + return True + + # Method 2: Development environment + if os.getenv("OPENADAPT_DEV", "").lower() in ("true", "1", "yes"): + return True + + # Method 3: Not running from executable (dev mode) + if not is_running_from_executable(): + return True + + # Method 4: Git repository present (development checkout) + if Path(".git").exists(): + return True + + # Method 5: Known internal email domain (if user identified) + # Note: Only if user voluntarily provided email + + # Method 6: CI/CD environment + ci_env_vars = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL"] + if any(os.getenv(var) for var in ci_env_vars): + return True + + return False +``` + +### Tag Application + +```python +def get_telemetry_tags() -> dict: + """Get standard tags for all telemetry events.""" + return { + "internal": is_internal_user(), + "environment": get_environment(), + "package_version": get_version(), + "python_version": platform.python_version(), + "os": platform.system(), + "os_version": platform.release(), + } +``` + +### Filtering in GlitchTip + +``` +# Filter out internal usage +tag:internal IS false + +# View only internal usage +tag:internal IS true + +# Combine with environment +tag:environment IS production AND tag:internal IS false +``` + +--- + +## Code Examples + +### Package Installation + +```toml +# pyproject.toml for any OpenAdapt package +[project] +dependencies = [ + "openadapt-telemetry>=0.1.0", +] + +[project.optional-dependencies] +# Telemetry is optional for those who want zero tracking +minimal = [] # Install without telemetry +``` + +### Telemetry Client Implementation + +```python +# src/openadapt_telemetry/client.py +"""Telemetry client for OpenAdapt packages.""" + +from __future__ import annotations + +import os +import platform +from functools import lru_cache +from pathlib import Path +from typing import Any, Callable, Optional + +import sentry_sdk +from sentry_sdk.types import Event, Hint + + +class TelemetryClient: + """Unified telemetry client for all OpenAdapt packages.""" + + _instance: Optional["TelemetryClient"] = None + + def __init__(self): + self._initialized = False + self._enabled = self._check_enabled() + self._internal = self._check_internal() + + @classmethod + def get_instance(cls) -> "TelemetryClient": + """Get singleton instance.""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def _check_enabled(self) -> bool: + """Check if telemetry should be enabled.""" + # Universal opt-out + if os.getenv("DO_NOT_TRACK", "").lower() in ("1", "true"): + return False + + # Package-specific opt-out + if os.getenv("OPENADAPT_TELEMETRY_ENABLED", "").lower() in ("false", "0", "no"): + return False + + return True + + def _check_internal(self) -> bool: + """Check if this is internal usage.""" + # Explicit flag + if os.getenv("OPENADAPT_INTERNAL", "").lower() in ("true", "1", "yes"): + return True + + # Development mode + if os.getenv("OPENADAPT_DEV", "").lower() in ("true", "1", "yes"): + return True + + # Git repo present (development checkout) + if Path(".git").exists(): + return True + + # CI environment + ci_vars = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "TRAVIS"] + if any(os.getenv(var) for var in ci_vars): + return True + + return False + + def initialize( + self, + dsn: Optional[str] = None, + package_name: str = "openadapt", + package_version: str = "unknown", + **kwargs, + ) -> None: + """Initialize the telemetry client.""" + if not self._enabled: + return + + if self._initialized: + return + + dsn = dsn or os.getenv( + "OPENADAPT_TELEMETRY_DSN", + "https://xxx@app.glitchtip.com/XXXX" # Default DSN + ) + + environment = os.getenv("OPENADAPT_TELEMETRY_ENVIRONMENT", "production") + sample_rate = float(os.getenv("OPENADAPT_TELEMETRY_SAMPLE_RATE", "1.0")) + traces_sample_rate = float(os.getenv("OPENADAPT_TELEMETRY_TRACES_SAMPLE_RATE", "0.01")) + + sentry_sdk.init( + dsn=dsn, + environment=environment, + sample_rate=sample_rate, + traces_sample_rate=traces_sample_rate, + send_default_pii=False, + before_send=self._before_send, + before_send_transaction=self._before_send_transaction, + **kwargs, + ) + + # Set default tags + sentry_sdk.set_tag("internal", self._internal) + sentry_sdk.set_tag("package", package_name) + sentry_sdk.set_tag("package_version", package_version) + sentry_sdk.set_tag("python_version", platform.python_version()) + sentry_sdk.set_tag("os", platform.system()) + sentry_sdk.set_tag("os_version", platform.release()) + + self._initialized = True + + def _before_send(self, event: Event, hint: Hint) -> Optional[Event]: + """Filter and sanitize events before sending.""" + # Scrub PII from stack traces + if "exception" in event: + self._scrub_exception(event["exception"]) + + return event + + def _before_send_transaction(self, event: Event, hint: Hint) -> Optional[Event]: + """Filter performance events.""" + return event + + def _scrub_exception(self, exception_data: dict) -> None: + """Remove PII from exception data.""" + if "values" not in exception_data: + return + + for value in exception_data["values"]: + if "stacktrace" in value and "frames" in value["stacktrace"]: + for frame in value["stacktrace"]["frames"]: + # Sanitize file paths + if "filename" in frame: + frame["filename"] = self._sanitize_path(frame["filename"]) + if "abs_path" in frame: + frame["abs_path"] = self._sanitize_path(frame["abs_path"]) + + @staticmethod + def _sanitize_path(path: str) -> str: + """Remove username from file paths.""" + import re + # macOS/Linux: /Users/username/ or /home/username/ + path = re.sub(r'/Users/[^/]+/', '/Users//', path) + path = re.sub(r'/home/[^/]+/', '/home//', path) + # Windows: C:\Users\username\ + path = re.sub(r'C:\\Users\\[^\\]+\\', 'C:\\Users\\\\', path) + return path + + def capture_exception(self, exception: Optional[Exception] = None, **kwargs) -> None: + """Capture an exception.""" + if not self._enabled: + return + sentry_sdk.capture_exception(exception, **kwargs) + + def capture_message(self, message: str, level: str = "info", **kwargs) -> None: + """Capture a message.""" + if not self._enabled: + return + sentry_sdk.capture_message(message, level=level, **kwargs) + + def capture_event(self, event_name: str, properties: Optional[dict] = None) -> None: + """Capture a custom event (feature usage).""" + if not self._enabled: + return + + properties = properties or {} + properties["event_name"] = event_name + sentry_sdk.capture_message( + f"event:{event_name}", + level="info", + extras=properties, + ) + + def set_user(self, user_id: str, **kwargs) -> None: + """Set user context (anonymous ID only).""" + if not self._enabled: + return + sentry_sdk.set_user({"id": user_id, **kwargs}) + + def set_tag(self, key: str, value: str) -> None: + """Set a custom tag.""" + if not self._enabled: + return + sentry_sdk.set_tag(key, value) + + def add_breadcrumb(self, message: str, category: str = "default", **kwargs) -> None: + """Add a breadcrumb for context.""" + if not self._enabled: + return + sentry_sdk.add_breadcrumb(message=message, category=category, **kwargs) + + +# Convenience singleton access +def get_telemetry() -> TelemetryClient: + """Get the telemetry client instance.""" + return TelemetryClient.get_instance() +``` + +### Decorator for Function Tracking + +```python +# src/openadapt_telemetry/decorators.py +"""Convenience decorators for telemetry.""" + +import functools +import time +from typing import Callable, Optional + +import sentry_sdk + +from .client import get_telemetry + + +def track_performance(name: Optional[str] = None): + """Decorator to track function performance.""" + def decorator(func: Callable) -> Callable: + operation_name = name or func.__name__ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + telemetry = get_telemetry() + + with sentry_sdk.start_transaction(op="function", name=operation_name): + start = time.perf_counter() + try: + return func(*args, **kwargs) + finally: + duration = time.perf_counter() - start + sentry_sdk.set_measurement("duration_ms", duration * 1000) + + return wrapper + return decorator + + +def track_errors(reraise: bool = True): + """Decorator to automatically capture exceptions.""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + get_telemetry().capture_exception(e) + if reraise: + raise + return wrapper + return decorator + + +def track_feature(feature_name: str): + """Decorator to track feature usage.""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + get_telemetry().capture_event( + f"feature:{feature_name}", + {"function": func.__name__}, + ) + return func(*args, **kwargs) + return wrapper + return decorator +``` + +### Package Integration Example + +```python +# src/openadapt_retrieval/__init__.py +"""OpenAdapt Retrieval - Multimodal demo retrieval.""" + +from openadapt_retrieval.embeddings import ( + BaseEmbedder, + CLIPEmbedder, + Qwen3VLEmbedder, + get_embedder, +) +from openadapt_retrieval.retriever import ( + DemoMetadata, + MultimodalDemoRetriever, + RetrievalResult, + VectorIndex, +) +from openadapt_retrieval.storage import EmbeddingStorage + +__version__ = "0.1.0" + +# Initialize telemetry on import (lazy, respects opt-out) +try: + from openadapt_telemetry import get_telemetry + get_telemetry().initialize( + package_name="openadapt-retrieval", + package_version=__version__, + ) +except ImportError: + # Telemetry package not installed (minimal install) + pass + +__all__ = [ + "BaseEmbedder", + "Qwen3VLEmbedder", + "CLIPEmbedder", + "get_embedder", + "MultimodalDemoRetriever", + "VectorIndex", + "RetrievalResult", + "DemoMetadata", + "EmbeddingStorage", +] +``` + +### Feature Usage Tracking Example + +```python +# In openadapt-retrieval/retriever/demo_retriever.py + +from openadapt_telemetry import get_telemetry, track_feature, track_performance + + +class MultimodalDemoRetriever: + """Retriever for multimodal demo search.""" + + @track_feature("retrieval.add_demo") + def add_demo( + self, + demo_id: str, + task: str, + screenshot: Optional[Union[str, Path, Image.Image]] = None, + **metadata, + ) -> None: + """Add a demo to the retrieval library.""" + # Implementation... + + @track_performance("retrieval.build_index") + def build_index(self) -> None: + """Build the FAISS index from stored demos.""" + try: + # Implementation... + get_telemetry().capture_event( + "retrieval.index_built", + {"num_demos": len(self._demos)}, + ) + except Exception as e: + get_telemetry().capture_exception(e) + raise + + @track_performance("retrieval.search") + def retrieve( + self, + task: str, + screenshot: Optional[Union[str, Path, Image.Image]] = None, + top_k: int = 5, + ) -> List[RetrievalResult]: + """Find similar demos for a given query.""" + # Implementation... +``` + +### CLI Opt-Out Information + +```python +# In CLI help text + +TELEMETRY_HELP = """ +OpenAdapt collects anonymous usage data to improve the software. + +What we collect: + - Error reports (exception types, stack traces) + - Performance metrics (timing, memory usage) + - Feature usage counts (which features are popular) + +What we NEVER collect: + - Screenshots or images + - Text you type or file contents + - Personal information (names, emails, IPs) + - API keys or passwords + +To disable telemetry: + - Set OPENADAPT_TELEMETRY_ENABLED=false + - Or set DO_NOT_TRACK=1 (universal standard) + +For more info: https://docs.openadapt.ai/telemetry +""" +``` + +--- + +## Migration Plan + +### Phase 1: Create Telemetry Package + +1. Create `openadapt-telemetry` package +2. Implement core client with GlitchTip/Sentry SDK +3. Add privacy filtering and scrubbing +4. Write comprehensive tests +5. Publish to PyPI + +### Phase 2: Update Meta-Package + +1. Add `openadapt-telemetry` as optional dependency +2. Update documentation +3. Add CLI telemetry status command + +### Phase 3: Integrate with Packages + +For each package (`capture`, `ml`, `evals`, `viewer`, `grounding`, `retrieval`, `privacy`): + +1. Add `openadapt-telemetry` dependency +2. Initialize telemetry in `__init__.py` +3. Add tracking to key operations +4. Test with telemetry enabled/disabled + +### Phase 4: Legacy Migration + +1. Update legacy error_reporting.py to use new module +2. Migrate PostHog events to unified system +3. Deprecate old telemetry code + +### Timeline + +| Phase | Duration | Milestone | +|-------|----------|-----------| +| Phase 1 | 1 week | Telemetry package published | +| Phase 2 | 2 days | Meta-package updated | +| Phase 3 | 2 weeks | All packages integrated | +| Phase 4 | 1 week | Legacy migration complete | + +--- + +## Testing Strategy + +### Unit Tests + +```python +# tests/test_telemetry.py + +import os +from unittest.mock import patch, MagicMock + +import pytest + +from openadapt_telemetry import TelemetryClient, get_telemetry + + +class TestTelemetryOptOut: + """Test that telemetry respects opt-out settings.""" + + def test_do_not_track_env(self): + """DO_NOT_TRACK=1 should disable telemetry.""" + with patch.dict(os.environ, {"DO_NOT_TRACK": "1"}): + client = TelemetryClient() + assert not client._enabled + + def test_explicit_disable(self): + """OPENADAPT_TELEMETRY_ENABLED=false should disable.""" + with patch.dict(os.environ, {"OPENADAPT_TELEMETRY_ENABLED": "false"}): + client = TelemetryClient() + assert not client._enabled + + def test_internal_detection(self): + """Internal users should be detected.""" + with patch.dict(os.environ, {"OPENADAPT_INTERNAL": "true"}): + client = TelemetryClient() + assert client._internal + + +class TestPrivacyScrubbing: + """Test that PII is properly scrubbed.""" + + def test_path_sanitization(self): + """File paths should have usernames removed.""" + client = TelemetryClient() + + assert client._sanitize_path("/Users/john/code/file.py") == "/Users//code/file.py" + assert client._sanitize_path("/home/alice/app/main.py") == "/home//app/main.py" + assert client._sanitize_path("C:\\Users\\bob\\code\\file.py") == "C:\\Users\\\\code\\file.py" +``` + +--- + +## References + +### GlitchTip + +- [GlitchTip Documentation](https://glitchtip.com/documentation/) +- [GlitchTip Installation Guide](https://glitchtip.com/documentation/install/) +- [Sentry SDK Documentation (GlitchTip compatible)](https://glitchtip.com/sdkdocs/python/) + +### Privacy & Ethics + +- [GDPR Telemetry Data Guidelines](https://www.activemind.legal/guides/telemetry-data/) +- [Linux Foundation Telemetry Policy](https://www.linuxfoundation.org/legal/telemetry-data-policy) +- [OpenTelemetry Handling Sensitive Data](https://opentelemetry.io/docs/security/handling-sensitive-data/) + +### Industry Standards + +- [DO_NOT_TRACK Environment Variable](https://consoledonottrack.com/) +- [Kedro Telemetry Plugin](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-telemetry) + +### Sentry SDK + +- [Sentry Python SDK](https://docs.sentry.io/platforms/python/) +- [Sentry Filtering](https://docs.sentry.io/platforms/python/configuration/filtering/) +- [Sentry Tags](https://docs.sentry.io/platforms/python/enriching-events/tags/) + +--- + +## Appendix: Configuration Reference + +### All Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DO_NOT_TRACK` | - | Universal opt-out (1 = disabled) | +| `OPENADAPT_TELEMETRY_ENABLED` | `true` | Enable/disable telemetry | +| `OPENADAPT_INTERNAL` | `false` | Tag as internal usage | +| `OPENADAPT_DEV` | `false` | Development mode | +| `OPENADAPT_TELEMETRY_DSN` | (default) | GlitchTip DSN | +| `OPENADAPT_TELEMETRY_ENVIRONMENT` | `production` | Environment name | +| `OPENADAPT_TELEMETRY_SAMPLE_RATE` | `1.0` | Error sampling rate | +| `OPENADAPT_TELEMETRY_TRACES_SAMPLE_RATE` | `0.01` | Performance sampling rate | + +### DSN Configuration + +The DSN (Data Source Name) should be stored securely and not committed to version control: + +```bash +# Development (use separate project) +export OPENADAPT_TELEMETRY_DSN="https://xxx@app.glitchtip.com/dev-project" + +# Production (use production project) +export OPENADAPT_TELEMETRY_DSN="https://xxx@app.glitchtip.com/prod-project" + +# Self-hosted +export OPENADAPT_TELEMETRY_DSN="https://xxx@glitchtip.your-domain.com/project" +``` diff --git a/docs/design/tray-logging.md b/docs/design/tray-logging.md new file mode 100644 index 000000000..b8ae7d937 --- /dev/null +++ b/docs/design/tray-logging.md @@ -0,0 +1,801 @@ +# OpenAdapt Tray: Logging & Action Storage + +This document supplements the main `openadapt-tray` design document with detailed specifications for logging, action history, telemetry integration, and storage considerations. + +## Table of Contents + +1. [Local Logging](#local-logging) +2. [Action History](#action-history) +3. [Telemetry Integration](#telemetry-integration) +4. [Privacy Considerations](#privacy-considerations) +5. [Storage Locations](#storage-locations) +6. [Integration with Existing Packages](#integration-with-existing-packages) + +--- + +## Local Logging + +### Platform-Specific Log Paths + +The tray application stores logs in platform-appropriate locations following OS conventions: + +| Platform | Log Directory | +|----------|---------------| +| macOS | `~/Library/Application Support/OpenAdapt/logs/` | +| Windows | `%APPDATA%/OpenAdapt/logs/` | +| Linux | `~/.local/share/openadapt/logs/` | + +### Log File Naming + +``` +openadapt-tray.log # Current log file +openadapt-tray.log.1 # Previous rotation (newest) +openadapt-tray.log.2 # Older rotation +... +openadapt-tray.log.5 # Oldest rotation +``` + +### Log Rotation Policy + +| Setting | Value | Rationale | +|---------|-------|-----------| +| **Max File Size** | 10 MB | Prevents disk space issues | +| **Max Backup Count** | 5 files | ~50 MB total log storage | +| **Rotation Trigger** | Size-based | Predictable disk usage | +| **Compression** | gzip for backups | Reduces storage footprint | + +### Log Retention Policy + +- **Active logs**: Rotated based on size (10 MB threshold) +- **Rotated logs**: Kept for 30 days or 5 rotations, whichever comes first +- **Crash logs**: Retained for 90 days for debugging +- **Automatic cleanup**: Old logs purged on app startup + +### Log Levels + +| Environment | Level | Description | +|-------------|-------|-------------| +| **Production** | `INFO` | Normal operations, errors, warnings | +| **Debug** | `DEBUG` | Verbose output including state changes | +| **Trace** | `TRACE` | Extremely verbose, including IPC messages | + +### Log Level Configuration + +```python +# Environment variable override +OPENADAPT_TRAY_LOG_LEVEL=DEBUG + +# Or via config.json +{ + "logging": { + "level": "INFO", + "console": false, + "file": true + } +} +``` + +### Log Format + +``` +2024-01-15 10:30:45.123 | INFO | tray.main:start_recording:42 - Recording session started +2024-01-15 10:30:45.456 | DEBUG | tray.menu:update_state:78 - Menu state updated: recording=True +2024-01-15 10:31:12.789 | ERROR | tray.capture:on_error:156 - Capture failed: Permission denied +``` + +Format specification: +``` +{timestamp} | {level:8} | {module}:{function}:{line} - {message} +``` + +### Implementation Example + +```python +import logging +from logging.handlers import RotatingFileHandler +from pathlib import Path +import platform +import sys + +def get_log_directory() -> Path: + """Get platform-appropriate log directory.""" + if platform.system() == "Darwin": + base = Path.home() / "Library" / "Application Support" + elif platform.system() == "Windows": + base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) + else: # Linux and others + base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) + + log_dir = base / "OpenAdapt" / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + return log_dir + +def setup_logging(level: str = "INFO") -> logging.Logger: + """Configure logging for the tray application.""" + logger = logging.getLogger("openadapt.tray") + logger.setLevel(getattr(logging, level.upper())) + + # File handler with rotation + log_file = get_log_directory() / "openadapt-tray.log" + file_handler = RotatingFileHandler( + log_file, + maxBytes=10 * 1024 * 1024, # 10 MB + backupCount=5, + encoding="utf-8", + ) + file_handler.setFormatter(logging.Formatter( + "{asctime} | {levelname:8} | {name}:{funcName}:{lineno} - {message}", + style="{", + datefmt="%Y-%m-%d %H:%M:%S", + )) + logger.addHandler(file_handler) + + return logger +``` + +--- + +## Action History + +### Overview + +The tray app maintains a local history of user interactions for: +- Auditing user actions +- Supporting undo/redo functionality +- Debugging session issues +- Syncing state with other OpenAdapt components + +### Tracked Actions + +| Action Type | Data Captured | Purpose | +|-------------|---------------|---------| +| `recording.start` | timestamp, task_name, settings | Session tracking | +| `recording.stop` | timestamp, duration, frame_count | Session completion | +| `recording.pause` | timestamp | Session state | +| `recording.resume` | timestamp | Session state | +| `training.start` | timestamp, model_type, demo_ids | Training tracking | +| `training.complete` | timestamp, duration, success | Training outcomes | +| `training.cancel` | timestamp, reason | Training interruptions | +| `settings.changed` | key, old_value, new_value | Configuration audit | +| `app.start` | timestamp, version, os_info | Lifecycle tracking | +| `app.stop` | timestamp, exit_reason | Lifecycle tracking | +| `error.occurred` | timestamp, error_type, context | Error tracking | + +### Storage Format + +Action history is stored in a local SQLite database for efficient querying and reliable storage. + +#### Database Schema + +```sql +-- Action history table +CREATE TABLE action_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL, -- ISO 8601 format + action_type TEXT NOT NULL, -- e.g., 'recording.start' + session_id TEXT, -- Groups related actions + data TEXT, -- JSON blob for action-specific data + synced INTEGER DEFAULT 0, -- Sync status with capture DB + created_at TEXT DEFAULT CURRENT_TIMESTAMP +); + +-- Index for common queries +CREATE INDEX idx_action_timestamp ON action_history(timestamp); +CREATE INDEX idx_action_type ON action_history(action_type); +CREATE INDEX idx_session_id ON action_history(session_id); +CREATE INDEX idx_synced ON action_history(synced); + +-- Session metadata table +CREATE TABLE sessions ( + id TEXT PRIMARY KEY, -- UUID + task_name TEXT, + started_at TEXT NOT NULL, + ended_at TEXT, + status TEXT DEFAULT 'active', -- active, completed, cancelled, error + frame_count INTEGER DEFAULT 0, + duration_seconds REAL, + capture_db_id TEXT -- Reference to openadapt-capture DB +); +``` + +#### Example Records + +```json +{ + "id": 1, + "timestamp": "2024-01-15T10:30:45.123Z", + "action_type": "recording.start", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "data": { + "task_name": "Fill out expense report", + "settings": { + "capture_screenshots": true, + "capture_audio": false, + "fps": 1 + } + }, + "synced": 0 +} +``` + +### Sync with openadapt-capture Database + +The tray app synchronizes action history with the capture package's database to maintain a unified record: + +```python +from pathlib import Path +import sqlite3 +from typing import Optional +import json + +class ActionHistorySync: + """Sync tray action history with capture database.""" + + def __init__(self, tray_db_path: Path, capture_db_path: Optional[Path] = None): + self.tray_db = tray_db_path + self.capture_db = capture_db_path + + def sync_session(self, session_id: str) -> bool: + """Sync a completed session to capture database.""" + if not self.capture_db or not self.capture_db.exists(): + return False + + with sqlite3.connect(self.tray_db) as tray_conn: + # Get unsynced actions for this session + actions = tray_conn.execute( + """ + SELECT id, timestamp, action_type, data + FROM action_history + WHERE session_id = ? AND synced = 0 + ORDER BY timestamp + """, + (session_id,) + ).fetchall() + + if not actions: + return True + + with sqlite3.connect(self.capture_db) as capture_conn: + # Insert into capture database's session_events table + for action_id, timestamp, action_type, data in actions: + capture_conn.execute( + """ + INSERT INTO session_events (timestamp, event_type, data, source) + VALUES (?, ?, ?, 'tray') + """, + (timestamp, action_type, data) + ) + capture_conn.commit() + + # Mark as synced + with sqlite3.connect(self.tray_db) as tray_conn: + tray_conn.executemany( + "UPDATE action_history SET synced = 1 WHERE id = ?", + [(a[0],) for a in actions] + ) + tray_conn.commit() + + return True +``` + +### Retention Policy + +| Data Type | Retention Period | Rationale | +|-----------|------------------|-----------| +| Action history | 90 days | Debugging and audit trail | +| Session metadata | 1 year | Long-term usage patterns | +| Synced records | 30 days (then delete) | Reduce redundancy | + +--- + +## Telemetry Integration + +### Reference Design + +For detailed telemetry implementation, see the comprehensive telemetry design at [docs/design/telemetry-design.md](./telemetry-design.md). + +### GlitchTip/Sentry Integration + +The tray app uses the shared `openadapt-telemetry` module for crash reporting and error tracking. + +```python +# Initialize telemetry in tray app +from openadapt_telemetry import get_telemetry + +def init_app(): + """Initialize the tray application.""" + telemetry = get_telemetry() + telemetry.initialize( + package_name="openadapt-tray", + package_version=__version__, + ) +``` + +### Error and Crash Reporting + +```python +from openadapt_telemetry import get_telemetry, track_errors + +class TrayApp: + @track_errors(reraise=True) + def start_recording(self, task_name: str) -> None: + """Start a recording session.""" + try: + # Recording logic... + pass + except PermissionError as e: + get_telemetry().capture_exception(e, tags={ + "action": "start_recording", + "platform": platform.system(), + }) + raise +``` + +### Anonymous Usage Analytics (Opt-In) + +Usage analytics are strictly opt-in and collect only aggregate, non-identifying data. + +#### Events Tracked + +| Event | Data Collected | Purpose | +|-------|----------------|---------| +| `tray.app_start` | timestamp, version, os, internal_flag | App lifecycle | +| `tray.app_stop` | timestamp, uptime_seconds, exit_reason | App lifecycle | +| `tray.recording_session` | duration_seconds, success, frame_count | Usage patterns | +| `tray.training_initiated` | model_type, demo_count | Feature usage | +| `tray.error` | error_type (no message), context | Error patterns | + +#### Event Implementation + +```python +from openadapt_telemetry import get_telemetry + +def track_recording_session(duration: float, success: bool, frame_count: int): + """Track recording session metrics (opt-in only).""" + telemetry = get_telemetry() + + if not telemetry.is_analytics_enabled(): + return + + telemetry.capture_event( + "tray.recording_session", + { + "duration_seconds": round(duration, 1), + "success": success, + "frame_count_bucket": bucket_count(frame_count), # 0-10, 10-50, 50-100, 100+ + } + ) + +def bucket_count(count: int) -> str: + """Bucket counts to avoid exact numbers (privacy).""" + if count <= 10: + return "0-10" + elif count <= 50: + return "10-50" + elif count <= 100: + return "50-100" + else: + return "100+" +``` + +--- + +## Privacy Considerations + +### Core Principles + +1. **Local-First**: All data stored locally by default +2. **No PII**: Never collect personally identifiable information +3. **No Content**: Never collect screenshots, recordings, or user input +4. **Explicit Consent**: Cloud sync and analytics require opt-in +5. **Transparency**: Users can inspect all stored data + +### What Is Never Collected or Transmitted + +| Data Type | Reason | +|-----------|--------| +| Screenshots | Highly sensitive, potential PII | +| Recorded actions | Contains user behavior data | +| Typed text | PII and sensitive content | +| File paths with usernames | PII leakage | +| IP addresses | Location identification | +| Hardware identifiers | Device fingerprinting | +| Window titles | May contain sensitive info | + +### Opt-In/Opt-Out Settings + +```json +// config.json +{ + "telemetry": { + "crash_reporting": true, // Enabled by default, can disable + "anonymous_analytics": false, // Disabled by default, opt-in + "cloud_sync": false // Disabled by default, opt-in + } +} +``` + +### Settings UI Integration + +The tray app settings menu should include clear telemetry controls: + +``` +Settings > Privacy +├── [x] Send crash reports (helps improve stability) +├── [ ] Share anonymous usage statistics +├── [ ] Sync settings across devices +└── [View collected data...] -> Opens local data directory +``` + +### Data Inspection + +Users can inspect all locally stored data: + +```python +def open_data_directory(): + """Open the OpenAdapt data directory in file explorer.""" + import subprocess + import platform + + data_dir = get_data_directory() + + if platform.system() == "Darwin": + subprocess.run(["open", str(data_dir)]) + elif platform.system() == "Windows": + subprocess.run(["explorer", str(data_dir)]) + else: + subprocess.run(["xdg-open", str(data_dir)]) +``` + +### Data Deletion + +Users can delete all local data: + +```python +def clear_all_data(keep_config: bool = True): + """Delete all OpenAdapt local data.""" + data_dir = get_data_directory() + + for item in data_dir.iterdir(): + if keep_config and item.name == "config.json": + continue + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + + logger.info("All local data cleared") +``` + +--- + +## Storage Locations + +### Directory Structure + +``` +macOS: ~/Library/Application Support/OpenAdapt/ +Windows: %APPDATA%/OpenAdapt/ +Linux: ~/.local/share/openadapt/ + +Contents: +├── logs/ # Application logs +│ ├── openadapt-tray.log # Current tray app log +│ ├── openadapt-tray.log.1 # Rotated logs +│ └── crash/ # Crash dumps +├── config.json # User settings and preferences +├── history.db # Action history (SQLite) +├── cache/ # Temporary files +│ ├── icons/ # Cached tray icons +│ └── temp/ # Temporary processing files +└── state/ # Persistent state + └── session.json # Current session state (for crash recovery) +``` + +### Storage Path Resolution + +```python +import os +import platform +from pathlib import Path +from typing import Dict + +def get_storage_paths() -> Dict[str, Path]: + """Get all storage paths for the current platform.""" + + if platform.system() == "Darwin": + base = Path.home() / "Library" / "Application Support" / "OpenAdapt" + elif platform.system() == "Windows": + appdata = os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming") + base = Path(appdata) / "OpenAdapt" + else: # Linux and others + xdg_data = os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share") + base = Path(xdg_data) / "openadapt" + + paths = { + "base": base, + "logs": base / "logs", + "crash_logs": base / "logs" / "crash", + "config": base / "config.json", + "history_db": base / "history.db", + "cache": base / "cache", + "state": base / "state", + } + + # Ensure directories exist + for key, path in paths.items(): + if key not in ("config", "history_db"): # Don't create files + path.mkdir(parents=True, exist_ok=True) + + return paths +``` + +### Config File Schema + +```json +{ + "$schema": "https://openadapt.ai/schemas/tray-config-v1.json", + "version": 1, + "logging": { + "level": "INFO", + "console": false, + "file": true, + "max_size_mb": 10, + "backup_count": 5 + }, + "telemetry": { + "crash_reporting": true, + "anonymous_analytics": false, + "cloud_sync": false + }, + "recording": { + "default_fps": 1, + "capture_audio": false, + "capture_screenshots": true, + "auto_pause_on_idle": true, + "idle_threshold_seconds": 30 + }, + "ui": { + "show_notifications": true, + "start_minimized": false, + "start_on_login": false + }, + "advanced": { + "capture_db_path": null, + "ml_model_path": null + } +} +``` + +--- + +## Integration with Existing Packages + +### Shared Telemetry Module + +The tray app uses the shared `openadapt-telemetry` module (see [telemetry-design.md](./telemetry-design.md)) for consistent telemetry across all OpenAdapt packages. + +```python +# pyproject.toml +[project] +dependencies = [ + "openadapt-telemetry>=0.1.0", +] +``` + +### Coordination with openadapt-capture + +The tray app coordinates with `openadapt-capture` for recording functionality: + +```python +from openadapt_capture import RecordingSession, CaptureConfig +from openadapt_tray.history import ActionHistory + +class TrayRecordingController: + """Bridge between tray UI and capture backend.""" + + def __init__(self): + self.history = ActionHistory() + self.current_session: Optional[RecordingSession] = None + + def start_recording(self, task_name: str, config: CaptureConfig) -> str: + """Start a new recording session.""" + import uuid + + session_id = str(uuid.uuid4()) + + # Log to action history + self.history.log_action( + action_type="recording.start", + session_id=session_id, + data={"task_name": task_name, "config": config.to_dict()} + ) + + # Start capture backend + self.current_session = RecordingSession( + session_id=session_id, + task_name=task_name, + config=config, + on_error=self._on_capture_error, + ) + self.current_session.start() + + return session_id + + def stop_recording(self) -> dict: + """Stop the current recording session.""" + if not self.current_session: + return {"error": "No active session"} + + result = self.current_session.stop() + + # Log completion + self.history.log_action( + action_type="recording.stop", + session_id=self.current_session.session_id, + data={ + "duration": result.duration, + "frame_count": result.frame_count, + "success": result.success, + } + ) + + # Sync with capture database + self.history.sync_session(self.current_session.session_id) + + self.current_session = None + return result.to_dict() + + def _on_capture_error(self, error: Exception): + """Handle capture errors.""" + get_telemetry().capture_exception(error) + self.history.log_action( + action_type="error.occurred", + session_id=self.current_session.session_id if self.current_session else None, + data={"error_type": type(error).__name__} + ) +``` + +### Surfacing Training Logs from openadapt-ml + +The tray app can display training progress and logs from the ML package: + +```python +from openadapt_ml import TrainingJob, TrainingStatus +from openadapt_tray.notifications import show_notification + +class TrayTrainingController: + """Bridge between tray UI and ML training backend.""" + + def __init__(self): + self.history = ActionHistory() + self.current_job: Optional[TrainingJob] = None + + def start_training(self, model_type: str, demo_ids: list[str]) -> str: + """Start a training job.""" + job_id = str(uuid.uuid4()) + + self.history.log_action( + action_type="training.start", + data={ + "job_id": job_id, + "model_type": model_type, + "demo_count": len(demo_ids), + } + ) + + self.current_job = TrainingJob( + job_id=job_id, + model_type=model_type, + demo_ids=demo_ids, + on_progress=self._on_training_progress, + on_complete=self._on_training_complete, + on_error=self._on_training_error, + ) + self.current_job.start() + + return job_id + + def _on_training_progress(self, progress: float, message: str): + """Handle training progress updates.""" + # Update tray icon or menu with progress + pass + + def _on_training_complete(self, result: TrainingStatus): + """Handle training completion.""" + self.history.log_action( + action_type="training.complete", + data={ + "job_id": self.current_job.job_id, + "duration": result.duration, + "success": result.success, + } + ) + + show_notification( + title="Training Complete", + message=f"Model trained successfully in {result.duration:.1f}s" + ) + + # Track telemetry (anonymous) + get_telemetry().capture_event( + "tray.training_complete", + {"model_type": self.current_job.model_type, "success": True} + ) + + def _on_training_error(self, error: Exception): + """Handle training errors.""" + get_telemetry().capture_exception(error) + + self.history.log_action( + action_type="training.error", + data={ + "job_id": self.current_job.job_id if self.current_job else None, + "error_type": type(error).__name__, + } + ) + + show_notification( + title="Training Failed", + message="An error occurred during training. Check logs for details." + ) +``` + +### Log Aggregation View + +The tray app can provide a unified view of logs from all OpenAdapt components: + +```python +from pathlib import Path +from typing import Iterator, NamedTuple +from datetime import datetime + +class LogEntry(NamedTuple): + timestamp: datetime + level: str + source: str # tray, capture, ml, etc. + message: str + +def aggregate_logs(max_entries: int = 1000) -> Iterator[LogEntry]: + """Aggregate logs from all OpenAdapt components.""" + + log_sources = { + "tray": get_storage_paths()["logs"] / "openadapt-tray.log", + "capture": get_capture_log_path(), # From openadapt-capture + "ml": get_ml_log_path(), # From openadapt-ml + } + + entries = [] + + for source, log_path in log_sources.items(): + if not log_path.exists(): + continue + + with open(log_path, "r") as f: + for line in f: + try: + entry = parse_log_line(line, source) + if entry: + entries.append(entry) + except Exception: + continue + + # Sort by timestamp and return most recent + entries.sort(key=lambda e: e.timestamp, reverse=True) + return iter(entries[:max_entries]) +``` + +--- + +## Summary + +This document defines the logging and storage architecture for the OpenAdapt tray application: + +1. **Local Logging**: Platform-specific paths with rotation and retention policies +2. **Action History**: SQLite-based storage for user interactions, synced with capture database +3. **Telemetry**: Integration with shared telemetry module for crash reporting and opt-in analytics +4. **Privacy**: Local-first approach with no PII collection and clear opt-in/opt-out controls +5. **Storage**: Organized directory structure following OS conventions +6. **Integration**: Seamless coordination with capture, ML, and telemetry packages + +For telemetry implementation details, refer to the comprehensive [telemetry design document](./telemetry-design.md). diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 3ac7ccc46..29e139fdf 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,15 +1,15 @@ # Quick Start -This guide walks you through recording a demonstration, training a model, and evaluating it. +This guide walks you through collecting a demonstration, learning a policy, and evaluating the agent. ## Prerequisites - OpenAdapt installed with required packages: `pip install openadapt[all]` - macOS users: [Grant required permissions](permissions.md) -## 1. Record a Demonstration +## 1. Collect a Demonstration -Start recording your screen and inputs: +Start capturing your screen and inputs: ```bash openadapt capture start --name my-task @@ -22,14 +22,14 @@ Now perform the task you want to automate: 3. Navigate menus 4. Complete your workflow -When finished, stop recording: +When finished, stop the capture: ```bash # Press Ctrl+C in the terminal, or: openadapt capture stop ``` -## 2. View the Recording +## 2. View the Trajectory Inspect what was captured: @@ -37,15 +37,15 @@ Inspect what was captured: openadapt capture view my-task ``` -This opens an HTML viewer showing: +This opens a trajectory viewer showing: -- Screenshots at each step -- Mouse and keyboard events +- Observations (screenshots) at each step +- Actions (mouse and keyboard events) - Timing information -## 3. List Your Captures +## 3. List Your Demonstrations -See all recorded demonstrations: +See all collected demonstrations: ```bash openadapt capture list @@ -59,25 +59,25 @@ my-task 45 2m 30s 2026-01-16 login-demo 23 1m 15s 2026-01-15 ``` -## 4. Train a Model +## 4. Learn a Policy -Train a model on your recorded demonstration: +Learn an agent policy from your demonstration trajectory: ```bash openadapt train start --capture my-task --model qwen3vl-2b ``` -Monitor training progress: +Monitor policy learning progress: ```bash openadapt train status ``` -Training creates a checkpoint file in `training_output/`. +Policy learning creates a checkpoint file in `training_output/`. -## 5. Evaluate the Model +## 5. Evaluate the Agent -Test your trained model on a benchmark: +Test your trained policy on a benchmark: ```bash openadapt eval run --checkpoint training_output/model.pt --benchmark waa @@ -103,7 +103,7 @@ openadapt eval run --agent api-claude --benchmark waa ## Complete Workflow Example -Here is a complete example from start to finish: +Here is a complete example demonstrating the full pipeline: ```bash # 1. Install OpenAdapt @@ -112,21 +112,21 @@ pip install openadapt[all] # 2. Check system requirements openadapt doctor -# 3. Record a task +# 3. Collect a demonstration openadapt capture start --name email-reply # ... perform the task ... # Press Ctrl+C to stop -# 4. View the recording +# 4. View the trajectory openadapt capture view email-reply -# 5. Train a model +# 5. Learn a policy openadapt train start --capture email-reply --model qwen3vl-2b -# 6. Wait for training to complete +# 6. Wait for policy learning to complete openadapt train status -# 7. Evaluate +# 7. Evaluate the agent openadapt eval run --checkpoint training_output/model.pt --benchmark waa ``` diff --git a/docs/index.md b/docs/index.md index 4aeb4c448..99f8bc665 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ OpenAdapt is the **open** source software **adapt**er between Large Multimodal Models (LMMs) and traditional desktop and web GUIs. -Record GUI demonstrations, train ML models, and evaluate agents - all from a unified CLI. +Collect human demonstrations, learn agent policies, and evaluate autonomous execution - all from a unified CLI. [Join Discord](https://discord.gg/yF527cQbDG){ .md-button .md-button--primary } [View on GitHub](https://github.com/OpenAdaptAI/OpenAdapt){ .md-button } @@ -15,24 +15,24 @@ Record GUI demonstrations, train ML models, and evaluate agents - all from a uni OpenAdapt bridges the gap between powerful AI models and everyday software automation. Instead of writing complex scripts or learning APIs, you simply: -1. **Record** - Demonstrate a task by doing it yourself -2. **Train** - Let OpenAdapt learn from your demonstration -3. **Deploy** - Run your trained agent to automate the task -4. **Evaluate** - Measure performance on standardized benchmarks +1. **Demonstrate** - Show the agent how to perform a task by doing it yourself +2. **Learn** - Let OpenAdapt learn an agent policy from your demonstration trajectory +3. **Execute** - Deploy your trained agent to autonomously perform the task +4. **Evaluate** - Measure agent performance on standardized benchmarks ```mermaid flowchart LR - subgraph Record["1. Record"] - A[User Demo] --> B[Capture] + subgraph Demonstrate["1. Demonstrate"] + A[Human Trajectory] --> B[Capture] end - subgraph Train["2. Train"] - B --> C[ML Model] + subgraph Learn["2. Learn"] + B --> C[Policy Learning] end - subgraph Deploy["3. Deploy"] - C --> D[Agent Policy] - D --> E[Action Replay] + subgraph Execute["3. Execute"] + C --> D[Trained Policy] + D --> E[Agent Deployment] end subgraph Evaluate["4. Evaluate"] @@ -53,7 +53,7 @@ flowchart LR Works with any Large Multimodal Model - Claude, GPT-4V, Gemini, Qwen-VL, or your own fine-tuned models. ### Learn from Demonstration -No prompting required. OpenAdapt learns directly from how you perform tasks, automatically generating the right prompts. +No manual prompt engineering required. OpenAdapt learns agent policies directly from your demonstration trajectories. ### Universal GUI Support Works with all desktop GUIs including native applications, web browsers, and virtualized environments. @@ -71,14 +71,14 @@ Install OpenAdapt with the features you need: pip install openadapt[all] # Everything ``` -Record a demonstration: +Collect a demonstration: ```bash openadapt capture start --name my-task # Perform your task, then press Ctrl+C ``` -Train a model: +Learn a policy: ```bash openadapt train start --capture my-task --model qwen3vl-2b @@ -100,12 +100,12 @@ OpenAdapt v1.0+ uses a **modular meta-package architecture**. The main `openadap | Package | Description | |---------|-------------| -| [openadapt-capture](packages/capture.md) | Event recording and storage | -| [openadapt-ml](packages/ml.md) | ML engine, training, inference | +| [openadapt-capture](packages/capture.md) | Demonstration collection and storage | +| [openadapt-ml](packages/ml.md) | Policy learning, training, inference | | [openadapt-evals](packages/evals.md) | Benchmark evaluation | -| [openadapt-viewer](packages/viewer.md) | HTML visualization | -| [openadapt-grounding](packages/grounding.md) | UI element localization | -| [openadapt-retrieval](packages/retrieval.md) | Multimodal demo retrieval | +| [openadapt-viewer](packages/viewer.md) | Trajectory visualization | +| [openadapt-grounding](packages/grounding.md) | UI element grounding | +| [openadapt-retrieval](packages/retrieval.md) | Trajectory retrieval | | [openadapt-privacy](packages/privacy.md) | PII/PHI scrubbing | See the full [Architecture Documentation](architecture.md) for detailed diagrams. diff --git a/docs/packages/capture.md b/docs/packages/capture.md index 67499f3fa..b27b6d846 100644 --- a/docs/packages/capture.md +++ b/docs/packages/capture.md @@ -1,6 +1,6 @@ # openadapt-capture -GUI recording, event capture, and storage. +Demonstration collection, observation-action capture, and storage. **Repository**: [OpenAdaptAI/openadapt-capture](https://github.com/OpenAdaptAI/openadapt-capture) @@ -14,17 +14,17 @@ pip install openadapt-capture ## Overview -The capture package records user interactions with desktop and web GUIs, including: +The capture package collects human demonstrations from desktop and web GUIs, including: -- Screenshots at configurable intervals -- Mouse events (clicks, movement, scrolling) -- Keyboard events (key presses, text input) +- Observations (screenshots) at configurable intervals +- Actions: mouse events (clicks, movement, scrolling) +- Actions: keyboard events (key presses, text input) - Window and application context -- Timing information +- Timing information for trajectory reconstruction ## CLI Commands -### Start Recording +### Start Demonstration Collection ```bash openadapt capture start --name my-task @@ -37,27 +37,27 @@ Options: - `--no-screenshots` - Disable screenshot capture - `--no-keyboard` - Disable keyboard capture -### Stop Recording +### Stop Demonstration Collection ```bash openadapt capture stop ``` -Or press `Ctrl+C` in the recording terminal. +Or press `Ctrl+C` in the capture terminal. -### List Captures +### List Demonstrations ```bash openadapt capture list ``` -### View a Capture +### View a Demonstration Trajectory ```bash openadapt capture view my-task ``` -### Delete a Capture +### Delete a Demonstration ```bash openadapt capture delete my-task @@ -75,41 +75,41 @@ session = CaptureSession(name="my-task") recorder = Recorder(session) recorder.start() -# ... user performs actions ... +# ... user demonstrates the task ... # Stop recording recorder.stop() -# Access captured data -events = session.get_events() -screenshots = session.get_screenshots() +# Access captured trajectory data +actions = session.get_actions() +observations = session.get_observations() # screenshots ``` ## Data Format -Captures are stored as JSON/Parquet files: +Demonstrations are stored as JSON/Parquet files: ``` -captures/ +demonstrations/ my-task/ metadata.json # Session metadata - events.parquet # Event data - screenshots/ # Screenshot images + actions.parquet # Action data (observation-action pairs) + observations/ # Screenshot images (observations) 0001.png 0002.png ... ``` -### Event Schema +### Action Schema ```python { - "timestamp": float, # Unix timestamp - "type": str, # "mouse_click", "key_press", etc. + "timestamp": float, # Unix timestamp + "action_type": str, # "click", "type", "scroll", etc. "data": { - # Event-specific data + # Action-specific data }, - "screenshot_id": int # Reference to screenshot + "observation_id": int # Reference to observation (screenshot) } ``` @@ -117,11 +117,11 @@ captures/ | Export | Description | |--------|-------------| -| `CaptureSession` | Manages a capture session | -| `Recorder` | Records user interactions | +| `CaptureSession` | Manages a demonstration collection session | +| `Recorder` | Captures observation-action pairs | | `Action` | Represents a user action | -| `MouseEvent` | Mouse event data | -| `KeyboardEvent` | Keyboard event data | +| `Observation` | Represents an observation (screenshot) | +| `Trajectory` | Sequence of observation-action pairs | ## Platform Support @@ -133,6 +133,6 @@ captures/ ## Related Packages -- [openadapt-privacy](privacy.md) - Scrub PII/PHI from captures -- [openadapt-viewer](viewer.md) - Visualize capture data -- [openadapt-ml](ml.md) - Train models on captures +- [openadapt-privacy](privacy.md) - Scrub PII/PHI from demonstrations +- [openadapt-viewer](viewer.md) - Visualize trajectories +- [openadapt-ml](ml.md) - Learn policies from demonstrations diff --git a/docs/packages/evals.md b/docs/packages/evals.md index 84f5fa4a7..d861f93a6 100644 --- a/docs/packages/evals.md +++ b/docs/packages/evals.md @@ -26,7 +26,7 @@ The evals package provides: ### Run Evaluation ```bash -# Evaluate a trained model +# Evaluate a trained policy openadapt eval run --checkpoint training_output/model.pt --benchmark waa # Evaluate an API agent @@ -35,7 +35,7 @@ openadapt eval run --agent api-claude --benchmark waa Options: -- `--checkpoint` - Path to model checkpoint +- `--checkpoint` - Path to trained policy checkpoint - `--agent` - Agent type (api-claude, api-gpt4v, custom) - `--benchmark` - Benchmark name (waa, osworld, etc.) - `--tasks` - Number of tasks to evaluate (default: all) @@ -88,7 +88,7 @@ from openadapt_evals import ApiAgent, BenchmarkAdapter, evaluate_agent_on_benchm # Create an API agent agent = ApiAgent.claude() -# Or load a trained model +# Or load a trained policy from openadapt_ml import AgentPolicy agent = AgentPolicy.from_checkpoint("model.pt") @@ -157,7 +157,7 @@ flowchart TB | `ApiAgent` | API-based agent (Claude, GPT-4V) | | `BenchmarkAdapter` | Benchmark interface | | `MockAdapter` | Mock benchmark for testing | -| `evaluate_agent_on_benchmark` | Evaluation function | +| `evaluate_agent_on_benchmark` | Agent evaluation function | | `EvalResults` | Evaluation results container | ## Metrics @@ -171,5 +171,5 @@ flowchart TB ## Related Packages -- [openadapt-ml](ml.md) - Train models to evaluate -- [openadapt-capture](capture.md) - Record training data +- [openadapt-ml](ml.md) - Learn policies to evaluate +- [openadapt-capture](capture.md) - Collect demonstrations diff --git a/docs/packages/grounding.md b/docs/packages/grounding.md index 7ef939cc6..0b52b019f 100644 --- a/docs/packages/grounding.md +++ b/docs/packages/grounding.md @@ -1,6 +1,6 @@ # openadapt-grounding -UI element localization for improved action accuracy. +UI element grounding for improved action accuracy. **Repository**: [OpenAdaptAI/openadapt-grounding](https://github.com/OpenAdaptAI/openadapt-grounding) @@ -14,7 +14,7 @@ pip install openadapt-grounding ## Overview -The grounding package provides UI element detection and localization to improve: +The grounding package provides UI element detection and grounding to improve: - Click accuracy by targeting element centers - Robustness to UI changes @@ -59,7 +59,7 @@ marked_image, element_map = som.create() # element_map: {1: "Submit button", 2: "Email field", ...} ``` -## Integration with ML +## Integration with Policy Execution ```python from openadapt_ml import AgentPolicy @@ -71,8 +71,9 @@ policy = AgentPolicy.from_checkpoint( grounding=ElementDetector() ) -# Predictions will use grounded coordinates -action = policy.predict(screenshot) +# Actions will use grounded coordinates +observation = load_screenshot() +action = policy.predict(observation) ``` ## CLI Commands @@ -122,5 +123,5 @@ openadapt ground som screenshot.png --output marked.png ## Related Packages -- [openadapt-ml](ml.md) - Use grounding in training and inference -- [openadapt-capture](capture.md) - Ground recorded captures +- [openadapt-ml](ml.md) - Use grounding in policy learning and execution +- [openadapt-capture](capture.md) - Apply grounding to demonstrations diff --git a/docs/packages/ml.md b/docs/packages/ml.md index c2261a709..479ea3aa0 100644 --- a/docs/packages/ml.md +++ b/docs/packages/ml.md @@ -1,6 +1,6 @@ # openadapt-ml -ML engine, training, and inference for GUI automation agents. +Policy learning, training, and inference for GUI automation agents. **Repository**: [OpenAdaptAI/openadapt-ml](https://github.com/OpenAdaptAI/openadapt-ml) @@ -17,13 +17,13 @@ pip install openadapt-ml The ML package provides: - Model adapters for various LMMs (Qwen-VL, LLaVA, etc.) -- Training infrastructure for supervised learning +- Policy learning infrastructure from demonstration trajectories - Inference engine for action prediction -- Agent policies for deployment +- Agent policies for autonomous execution ## CLI Commands -### Start Training +### Start Policy Learning ```bash openadapt train start --capture my-task --model qwen3vl-2b @@ -31,19 +31,19 @@ openadapt train start --capture my-task --model qwen3vl-2b Options: -- `--capture` - Name of the capture to train on (required) +- `--capture` - Name of the demonstration to learn from (required) - `--model` - Model architecture (required) - `--epochs` - Number of training epochs (default: 10) - `--batch-size` - Batch size (default: 4) - `--output` - Output directory (default: training_output/) -### Check Training Status +### Check Policy Learning Status ```bash openadapt train status ``` -### Stop Training +### Stop Policy Learning ```bash openadapt train stop @@ -72,32 +72,32 @@ from openadapt_ml import QwenVLAdapter, Trainer, AgentPolicy # Load a pre-trained model adapter = QwenVLAdapter.from_pretrained("qwen3vl-2b") -# Create trainer +# Create trainer for policy learning trainer = Trainer( model=adapter, - capture_name="my-task", + demonstration="my-task", # demonstration name epochs=10 ) -# Train +# Learn policy from demonstration trajectory checkpoint_path = trainer.train() -# Load for inference +# Load trained policy for execution policy = AgentPolicy.from_checkpoint(checkpoint_path) -# Predict next action -screenshot = load_screenshot() -action = policy.predict(screenshot) +# Predict next action from observation +observation = load_screenshot() +action = policy.predict(observation) ``` -## Training Pipeline +## Policy Learning Pipeline ```mermaid flowchart LR subgraph Input - CAP[Capture Data] - SS[Screenshots] - EV[Events] + DEMO[Demonstration] + OBS[Observations] + ACT[Actions] end subgraph Processing @@ -106,20 +106,20 @@ flowchart LR TOK[Tokenization] end - subgraph Training + subgraph Learning FWD[Forward Pass] LOSS[Loss Calculation] OPT[Optimization] end subgraph Output - CKPT[Checkpoint] + CKPT[Trained Policy] LOG[Training Logs] end - CAP --> DL - SS --> DL - EV --> DL + DEMO --> DL + OBS --> DL + ACT --> DL DL --> AUG AUG --> TOK TOK --> FWD @@ -135,9 +135,9 @@ flowchart LR |--------|-------------| | `QwenVLAdapter` | Qwen-VL model adapter | | `LLaVAAdapter` | LLaVA model adapter | -| `Trainer` | Training infrastructure | -| `AgentPolicy` | Inference policy | -| `train_supervised` | Training function | +| `Trainer` | Policy learning infrastructure | +| `AgentPolicy` | Trained policy for execution | +| `learn_from_demonstrations` | Policy learning function | ## Hardware Requirements @@ -149,6 +149,6 @@ flowchart LR ## Related Packages -- [openadapt-capture](capture.md) - Record training data -- [openadapt-evals](evals.md) - Evaluate trained models -- [openadapt-retrieval](retrieval.md) - Few-shot retrieval for training +- [openadapt-capture](capture.md) - Collect demonstrations +- [openadapt-evals](evals.md) - Evaluate trained policies +- [openadapt-retrieval](retrieval.md) - Trajectory retrieval for few-shot policy learning diff --git a/docs/packages/privacy.md b/docs/packages/privacy.md index 9bbf2be9c..2a5aff056 100644 --- a/docs/packages/privacy.md +++ b/docs/packages/privacy.md @@ -44,7 +44,7 @@ The privacy package provides: ## CLI Commands -### Scrub a Capture +### Scrub a Demonstration ```bash openadapt privacy scrub my-task @@ -78,8 +78,8 @@ from openadapt_privacy import Scrubber, PIIDetector # Create a scrubber scrubber = Scrubber(mode="blur") -# Scrub a capture -scrubber.scrub_capture("my-task", output_dir="scrubbed/") +# Scrub a demonstration +scrubber.scrub_demonstration("my-task", output_dir="scrubbed/") # Or scrub individual images scrubbed_image = scrubber.scrub_image(screenshot_path) @@ -106,10 +106,10 @@ session = CaptureSession( recorder = Recorder(session) recorder.start() -# ... recording ... +# ... demonstration collection ... recorder.stop() -# Captures are automatically scrubbed +# Demonstrations are automatically scrubbed ``` ## Redaction Modes @@ -152,5 +152,5 @@ This package helps with compliance for: ## Related Packages -- [openadapt-capture](capture.md) - Record demonstrations to scrub -- [openadapt-viewer](viewer.md) - View scrubbed captures +- [openadapt-capture](capture.md) - Collect demonstrations to scrub +- [openadapt-viewer](viewer.md) - View scrubbed demonstrations diff --git a/docs/packages/retrieval.md b/docs/packages/retrieval.md index 1c85b4e9f..ae167ebf0 100644 --- a/docs/packages/retrieval.md +++ b/docs/packages/retrieval.md @@ -1,6 +1,6 @@ # openadapt-retrieval -Multimodal demonstration retrieval for few-shot prompting. +Multimodal trajectory retrieval for few-shot policy learning. **Repository**: [OpenAdaptAI/openadapt-retrieval](https://github.com/OpenAdaptAI/openadapt-retrieval) @@ -16,47 +16,47 @@ pip install openadapt-retrieval The retrieval package enables: -- Semantic search over captured demonstrations -- Few-shot example selection for prompting +- Semantic search over demonstration trajectories +- Few-shot example selection for policy learning - Multimodal similarity (text + image) - Demonstration library management ## Use Cases -### Few-Shot Prompting +### Few-Shot Policy Learning -Find similar demonstrations to use as examples when prompting an LMM. +Find similar demonstrations to use as examples when learning agent policies. -### Transfer Learning +### Trajectory Transfer -Retrieve relevant demonstrations for new tasks. +Retrieve relevant demonstration trajectories for new tasks. ### Demonstration Discovery -Search your library of captured demonstrations. +Search your library of demonstration trajectories. ## Python API ```python from openadapt_retrieval import DemoIndex, retrieve_similar -# Build an index over your captures +# Build an index over your demonstrations index = DemoIndex() -index.add_captures(["task-1", "task-2", "task-3"]) +index.add_demonstrations(["task-1", "task-2", "task-3"]) -# Retrieve similar demonstrations -screenshot = load_screenshot() +# Retrieve similar demonstration trajectories +observation = load_screenshot() similar = index.search( - query_image=screenshot, + query_image=observation, query_text="click the submit button", top_k=3 ) for result in similar: - print(f"{result.capture_name}: {result.similarity:.2f}") + print(f"{result.demonstration_name}: {result.similarity:.2f}") ``` -### Integration with ML +### Integration with Policy Learning ```python from openadapt_ml import AgentPolicy @@ -69,8 +69,9 @@ policy = AgentPolicy.from_checkpoint( retrieval_index=index ) -# Predictions include relevant examples -action = policy.predict(screenshot, use_retrieval=True) +# Policy uses similar trajectory examples for few-shot learning +observation = load_screenshot() +action = policy.predict(observation, use_retrieval=True) ``` ## CLI Commands @@ -87,7 +88,7 @@ openadapt retrieval index --captures task-1 task-2 task-3 openadapt retrieval search --image screenshot.png --text "click submit" ``` -### List Indexed Captures +### List Indexed Demonstrations ```bash openadapt retrieval list @@ -97,7 +98,7 @@ openadapt retrieval list | Export | Description | |--------|-------------| -| `DemoIndex` | Demonstration index | +| `DemoIndex` | Demonstration trajectory index | | `retrieve_similar` | Similarity search | | `Embedding` | Vector embedding | | `SearchResult` | Search result data | @@ -118,7 +119,7 @@ Indexes are stored as pickle files: indexes/ demo_index.pkl # Main index embeddings.npy # Vector embeddings - metadata.json # Capture metadata + metadata.json # Demonstration metadata ``` ## Performance @@ -131,5 +132,5 @@ indexes/ ## Related Packages -- [openadapt-capture](capture.md) - Record demonstrations to index -- [openadapt-ml](ml.md) - Use retrieval in training +- [openadapt-capture](capture.md) - Collect demonstrations to index +- [openadapt-ml](ml.md) - Use retrieval in policy learning diff --git a/docs/packages/viewer.md b/docs/packages/viewer.md index 54646ceb8..2314413d3 100644 --- a/docs/packages/viewer.md +++ b/docs/packages/viewer.md @@ -1,6 +1,6 @@ # openadapt-viewer -HTML visualization components for capture data. +Trajectory visualization components for demonstration data. **Repository**: [OpenAdaptAI/openadapt-viewer](https://github.com/OpenAdaptAI/openadapt-viewer) @@ -16,14 +16,14 @@ pip install openadapt-viewer The viewer package provides: -- HTML-based visualization of captures -- Interactive replay viewer -- Event timeline display -- Screenshot galleries +- HTML-based visualization of demonstration trajectories +- Interactive trajectory viewer +- Action timeline display +- Observation galleries ## CLI Commands -### View a Capture +### View a Demonstration Trajectory ```bash openadapt capture view my-task @@ -49,8 +49,8 @@ Access the dashboard at `http://localhost:8080`. ```python from openadapt_viewer import PageBuilder, HTMLBuilder -# Build a viewer page for a capture -builder = PageBuilder(capture_name="my-task") +# Build a viewer page for a demonstration +builder = PageBuilder(demonstration="my-task") html = builder.build() # Save to file @@ -59,29 +59,29 @@ with open("viewer.html", "w") as f: # Or use HTMLBuilder for custom visualizations html_builder = HTMLBuilder() -html_builder.add_screenshot(screenshot_path, events) -html_builder.add_timeline(events) +html_builder.add_observation(screenshot_path, actions) +html_builder.add_timeline(actions) html = html_builder.render() ``` ## Viewer Features -### Screenshot Gallery +### Observation Gallery -Browse all captured screenshots with navigation controls. +Browse all captured observations (screenshots) with navigation controls. -### Event Timeline +### Action Timeline Interactive timeline showing: -- Mouse events (clicks, movement) -- Keyboard events (key presses) -- Screenshot timestamps -- Event metadata +- Mouse actions (clicks, movement) +- Keyboard actions (key presses) +- Observation timestamps +- Action metadata -### Replay Controls +### Trajectory Playback Controls -- Play/pause replay +- Play/pause trajectory playback - Speed controls (0.5x, 1x, 2x) - Step forward/backward - Jump to specific time @@ -90,16 +90,16 @@ Interactive timeline showing: - Export as HTML (static) - Export as video (MP4) -- Export event log (JSON) +- Export trajectory log (JSON) ## Key Exports | Export | Description | |--------|-------------| -| `PageBuilder` | Builds viewer pages | +| `PageBuilder` | Builds trajectory viewer pages | | `HTMLBuilder` | Low-level HTML construction | -| `TimelineWidget` | Timeline visualization | -| `ScreenshotGallery` | Screenshot browser | +| `TimelineWidget` | Action timeline visualization | +| `ObservationGallery` | Observation browser | ## Customization @@ -109,27 +109,27 @@ Interactive timeline showing: from openadapt_viewer import PageBuilder, Theme builder = PageBuilder( - capture_name="my-task", + demonstration="my-task", theme=Theme.DARK # or Theme.LIGHT ) ``` -### Custom Event Rendering +### Custom Action Rendering ```python -from openadapt_viewer import PageBuilder, EventRenderer +from openadapt_viewer import PageBuilder, ActionRenderer -class CustomRenderer(EventRenderer): - def render_mouse_click(self, event): - return f"
{event}
" +class CustomRenderer(ActionRenderer): + def render_click(self, action): + return f"
{action}
" builder = PageBuilder( - capture_name="my-task", + demonstration="my-task", renderer=CustomRenderer() ) ``` ## Related Packages -- [openadapt-capture](capture.md) - Record data to visualize +- [openadapt-capture](capture.md) - Collect demonstrations to visualize - [openadapt-privacy](privacy.md) - Scrub sensitive data before viewing diff --git a/docs/publication-roadmap.md b/docs/publication-roadmap.md new file mode 100644 index 000000000..8eb076530 --- /dev/null +++ b/docs/publication-roadmap.md @@ -0,0 +1,527 @@ +# OpenAdapt Publication Roadmap + +**Version**: 1.0 +**Date**: January 2026 +**Status**: Active Planning +**Author**: OpenAdapt Research Team + +--- + +## Executive Summary + +This roadmap outlines the publication strategy for OpenAdapt's core research contributions. The primary innovation is **demonstration-conditioned GUI agents**, which achieve dramatic accuracy improvements (33% to 100% first-action accuracy) by conditioning VLM agents on human demonstrations rather than relying solely on natural language instructions. + +--- + +## Table of Contents + +1. [Publishable Contributions](#1-publishable-contributions) +2. [Publication Timeline](#2-publication-timeline) +3. [Required Experiments](#3-required-experiments) +4. [Author Contributions](#4-author-contributions) +5. [Venue Analysis](#5-venue-analysis) +6. [Existing Drafts and Assets](#6-existing-drafts-and-assets) + +--- + +## 1. Publishable Contributions + +### 1.1 Demo-Conditioned GUI Agents (Core Innovation) + +**The Big Result**: Demonstration conditioning improves first-action accuracy from 33% to 100% on macOS tasks, with expected similar improvements (+30-50pp) on Windows Agent Arena (WAA). + +**Key Claims**: +- Demonstrations capture implicit knowledge that natural language prompts cannot convey +- Demo retrieval enables automatic selection of relevant examples from a library +- The "show, don't tell" paradigm reduces prompt engineering burden +- Works with any VLM backend (Claude, GPT, Gemini, Qwen-VL) + +**Research Questions Addressed**: +1. How much does demonstration context improve GUI agent performance? +2. Can we automatically retrieve relevant demonstrations for new tasks? +3. What is the transfer efficiency between similar tasks across platforms? + +**Preliminary Results** (from `/Users/abrichr/oa/src/openadapt-ml/docs/experiments/`): +- Zero-shot (instruction only): 33% first-action accuracy +- Demo-conditioned: 100% first-action accuracy (+67pp improvement) +- Demo persists across ALL steps (critical P0 fix for episode success) + +**WAA Predictions** (from experiment design): +- Zero-shot expected: 10-20% task success (consistent with SOTA ~19.5%) +- Demo-conditioned expected: 40-70% task success (+30-50pp improvement) + +--- + +### 1.2 Modular Open-Source Architecture (Meta-Package Design) + +**Contribution**: A composable, model-agnostic architecture for GUI automation research. + +**Key Components**: +| Package | Responsibility | Key Innovation | +|---------|---------------|----------------| +| `openadapt-capture` | GUI recording | Cross-platform event + a11y tree capture | +| `openadapt-ml` | Training & inference | Model-agnostic VLM adapters | +| `openadapt-evals` | Benchmark evaluation | Unified adapter for WAA, WebArena | +| `openadapt-retrieval` | Demo search | Multimodal (text+image) embedding with Qwen3-VL | +| `openadapt-grounding` | Element localization | Multiple providers (OmniParser, Florence2, Gemini) | +| `openadapt-viewer` | Visualization | Interactive HTML trajectory viewer | +| `openadapt-privacy` | PII scrubbing | Privacy-preserving demonstration storage | + +**Technical Highlights**: +- Abstraction ladder: Literal -> Symbolic -> Template -> Semantic -> Goal +- Process graph representations for temporal context +- Three-phase architecture: DEMONSTRATE -> LEARN -> EXECUTE +- Feedback loops for continuous improvement + +**Prior Art Comparison**: +| System | Open Source | Modular | Demo-Conditioned | Multi-VLM | +|--------|------------|---------|------------------|-----------| +| OpenAdapt | Yes | Yes | **Yes** | Yes | +| Claude Computer Use | No | No | No | No | +| UFO | Partial | No | No | No | +| SeeAct | Yes | No | No | No | + +--- + +### 1.3 Benchmark Evaluation Framework (WAA Integration) + +**Contribution**: Unified evaluation infrastructure for GUI agent benchmarks. + +**Key Features**: +- `BenchmarkAdapter` abstract interface for any benchmark +- `WAALiveAdapter` with HTTP-based `/evaluate` endpoint +- `ApiAgent` supporting Claude, GPT-5.1, Gemini backends +- `RetrievalAugmentedAgent` for automatic demo selection +- Execution trace collection with screenshots per step +- HTML viewer for result analysis + +**Benchmark Coverage**: +| Benchmark | Status | Tasks | Domain | +|-----------|--------|-------|--------| +| Windows Agent Arena (WAA) | Implemented | 154 tasks | Windows desktop | +| Mock Benchmark | Implemented | N tasks | Testing | +| WebArena | Partial | 812 tasks | Web browser | +| OSWorld | Planned | 369 tasks | Cross-platform | + +**WAA Task Selection** (from experiment design): +- 10 carefully selected tasks across 4 enterprise-relevant domains +- Browser/Edge (3 tasks): Privacy settings, bookmarks, font size +- Office/LibreOffice (3 tasks): Fill blanks, charts, alignment +- Settings (2 tasks): Notifications, Night Light scheduling +- File Explorer (2 tasks): Archive creation, view changes + +--- + +### 1.4 Multimodal Retrieval for Demo Conditioning + +**Contribution**: Automatic demonstration retrieval using VLM embeddings. + +**Technical Approach**: +- **Embedder**: Qwen3-VL-Embedding with Matryoshka Representation Learning (MRL) +- **Index**: FAISS vector index with cosine similarity +- **Query**: Multimodal (task text + current screenshot) +- **Reranking**: Cross-encoder for top-k refinement + +**Key Classes** (from `openadapt-retrieval`): +```python +# Core retrieval interface +retriever = MultimodalDemoRetriever(embedding_dim=512) +retriever.add_demo(demo_id, task, screenshot, app_name) +retriever.build_index() +results = retriever.retrieve(task, screenshot, top_k=3) +``` + +**Performance Considerations**: +- Qwen3-VL: ~6-8 GB VRAM, ~50-200ms per embedding +- CLIP fallback: ~2 GB VRAM, ~10-50ms per embedding +- Flexible dimensions via MRL: 256, 512, 1024, 2048 + +--- + +## 2. Publication Timeline + +### Phase 1: Short-Term (Q1 2026) + +#### 2.1.1 Blog Post / Technical Report + +**Target**: January-February 2026 +**Venue**: OpenAdapt blog, HuggingFace, towards data science +**Effort**: 1-2 weeks + +**Content**: +- Demo-conditioned GUI agents: The "show, don't tell" paradigm +- Preliminary results (33% -> 100% accuracy) +- Open-source release announcement +- Interactive demo with viewer + +**Deliverables**: +- [ ] Write blog post (~2000 words) +- [ ] Create figures (architecture diagram, accuracy comparison) +- [ ] Record demo video (2-3 minutes) +- [ ] Publish to blog + cross-post to HN, Reddit, Twitter + +--- + +#### 2.1.2 arXiv Preprint + +**Target**: February-March 2026 +**Venue**: arXiv cs.AI, cs.HC +**Effort**: 3-4 weeks + +**Title Options**: +1. "Show, Don't Tell: Demonstration-Conditioned GUI Automation with Vision-Language Models" +2. "OpenAdapt: An Open Framework for Demo-Conditioned GUI Agents" +3. "From Demonstrations to Actions: Retrieval-Augmented GUI Automation" + +**Existing Drafts**: +- `/Users/abrichr/oa/src/omnimcp/paper/omnimcp_whitepaper.tex` - Spatial-temporal framework +- `/Users/abrichr/oa/src/omnimcp/paper/omnimcp_arxiv.tex` - Full arXiv draft (1056 lines) + +**Structure** (based on existing drafts): +1. Abstract +2. Introduction (demo-conditioning motivation) +3. Related Work (GUI automation, VLM agents, PbD) +4. Method + - Architecture overview + - Demo-conditioned prompting + - Retrieval-augmented generation +5. Experiments + - macOS demo experiment + - WAA benchmark evaluation + - Ablation studies +6. Results + - First-action accuracy + - Episode success rate + - Transfer across platforms +7. Discussion & Limitations +8. Conclusion + +**Deliverables**: +- [ ] Complete WAA experiments (10 tasks x 2 conditions) +- [ ] Update existing LaTeX draft with new results +- [ ] Add retrieval system section +- [ ] Create supplementary materials (code, demos) +- [ ] Submit to arXiv + +--- + +### Phase 2: Medium-Term (Q2-Q3 2026) + +#### 2.2.1 Workshop Paper + +**Target**: April-June 2026 +**Venues** (submission deadlines vary): +| Venue | Conference | Deadline | Focus | +|-------|-----------|----------|-------| +| LLM Agents Workshop | ICML 2026 | ~March | Agent architectures | +| Human-AI Workshop | CHI 2026 | ~Dec 2025 | Human-AI collaboration | +| AutoML Workshop | NeurIPS 2026 | ~Sept | Automation | + +**Format**: 4-8 pages + references +**Effort**: 2-3 weeks (building on preprint) + +**Focus**: Demo retrieval and conditioning system +**Novelty**: Multimodal retrieval for GUI automation + +--- + +#### 2.2.2 Demo Paper (CHI/UIST) + +**Target**: CHI 2027 or UIST 2026 +**Venues**: +| Venue | Deadline | Acceptance Rate | +|-------|----------|-----------------| +| CHI Demo Track | Sept 2026 | ~50% | +| UIST Demo Track | April 2026 | ~40% | + +**Format**: 2-4 pages + live demo +**Effort**: 2 weeks for paper, 1 week for demo prep + +**Demo Content**: +1. Record a demonstration (any application) +2. Show retrieval selecting similar demos +3. Execute task with demo conditioning +4. Visualize predictions in viewer + +**Deliverables**: +- [ ] Prepare stable demo environment +- [ ] Create video walkthrough +- [ ] Write demo paper +- [ ] Prepare live demo hardware/software + +--- + +### Phase 3: Long-Term (Q4 2026 - 2027) + +#### 2.3.1 Full Conference Paper + +**Target**: NeurIPS 2026, ICML 2027, or ICLR 2027 +**Effort**: 3-6 months + +**Venues**: +| Venue | Deadline | Page Limit | Focus | +|-------|----------|------------|-------| +| NeurIPS | May 2026 | 9+refs | ML methods | +| ICML | Feb 2027 | 8+refs | ML methods | +| ICLR | Oct 2026 | 8+refs | Representations | +| AAAI | Aug 2026 | 7+refs | AI systems | +| ACL | Feb 2027 | 8+refs | NLP/multimodal | + +**Contribution Options**: + +**Option A: Demo-Conditioning Method Paper** (NeurIPS/ICML) +- Focus: Retrieval-augmented demo conditioning +- Experiments: WAA, WebArena, OSWorld comparison +- Ablations: Retrieval methods, embedding models, k values +- Baselines: Zero-shot, few-shot, fine-tuned + +**Option B: Systems Paper** (MLSys) +- Focus: Modular architecture for GUI automation +- Experiments: Latency, throughput, grounding accuracy +- Comparisons: End-to-end vs modular approaches + +**Option C: HCI Paper** (CHI Full) +- Focus: Human-AI collaboration in task automation +- User study: Demo creation time, task success, trust +- Qualitative: User preferences, failure modes + +--- + +## 3. Required Experiments + +### 3.1 Completed Experiments + +| Experiment | Status | Location | Result | +|------------|--------|----------|--------| +| macOS demo-conditioning | Done | `openadapt-ml/docs/experiments/` | 33% -> 100% | +| Demo prompt format | Done | Same | Behavior-only format best | +| API baselines | Done | `openadapt-evals` | Claude, GPT working | + +--- + +### 3.2 Required for arXiv (P0) + +| Experiment | Description | Effort | Status | +|------------|-------------|--------|--------| +| WAA zero-shot baseline | 10 tasks, no demos | 2-3 hours | Pending | +| WAA demo-conditioned | 10 tasks, with demos | 2-3 hours | Pending | +| Demo creation | Write demos for 10 WAA tasks | 4-6 hours | Design complete | +| Statistical analysis | Significance tests, confidence intervals | 1-2 hours | Pending | + +**WAA Task List** (from experiment design): +1. Edge: Do Not Track +2. Edge: Bookmark to bar +3. Edge: Font size +4. LibreOffice Calc: Fill blanks +5. LibreOffice Calc: Chart creation +6. LibreOffice Writer: Center align +7. Settings: Notifications off +8. Settings: Night Light schedule +9. File Explorer: Archive folder +10. File Explorer: Details view + +--- + +### 3.3 Required for Workshop/Demo Paper (P1) + +| Experiment | Description | Effort | Status | +|------------|-------------|--------|--------| +| Retrieval accuracy | Measure if correct demo retrieved | 1 day | Pending | +| Retrieval latency | Embedding + search time | 2 hours | Pending | +| Cross-domain transfer | Demo from app A helps app B | 1 week | Pending | +| Demo library size | Performance vs library size | 2-3 days | Pending | + +--- + +### 3.4 Required for Full Conference Paper (P2) + +| Experiment | Description | Effort | Status | +|------------|-------------|--------|--------| +| WebArena evaluation | 100+ web tasks | 1-2 weeks | Pending | +| OSWorld evaluation | Cross-platform tasks | 2-3 weeks | Pending | +| Fine-tuning comparison | Demo prompting vs fine-tuning | 2-4 weeks | Pending | +| Ablation: VLM backend | Claude vs GPT vs Gemini | 1 week | Partial | +| Ablation: Embedding model | Qwen3-VL vs CLIP vs ColPali | 1 week | Pending | +| Ablation: Demo format | Full trace vs behavior-only | 3 days | Partial | +| User study | N=20-30 participants | 2-4 weeks | Pending | + +--- + +## 4. Author Contributions + +### 4.1 Proposed Author Order + +**Lead Authors** (equal contribution): +1. **Richard Abrich** - Architecture, demo-conditioning, experiments +2. **[Contributor 2]** - Retrieval system, embeddings + +**Contributing Authors**: +3. **[Contributor 3]** - WAA benchmark integration +4. **[Contributor 4]** - Grounding module +5. **[Contributor 5]** - Viewer and visualization + +**Acknowledgments**: +- OmniParser team (Microsoft) +- Windows Agent Arena team (Microsoft) +- Open-source contributors + +--- + +### 4.2 Contribution Matrix + +| Contribution | Lead | Contributors | +|--------------|------|--------------| +| Architecture design | RA | - | +| Demo-conditioning method | RA | - | +| Retrieval system | - | - | +| WAA integration | RA | - | +| Grounding providers | RA | - | +| Experiments: macOS | RA | - | +| Experiments: WAA | RA | - | +| Writing: Introduction | RA | - | +| Writing: Method | RA | - | +| Writing: Experiments | RA | - | +| Figures and diagrams | RA | - | +| Code open-sourcing | RA | - | + +--- + +## 5. Venue Analysis + +### 5.1 Target Venues by Contribution Type + +#### Systems/Architecture +| Venue | Deadline | Fit | Notes | +|-------|----------|-----|-------| +| MLSys | Jan 2026 | Good | Modular architecture focus | +| OSDI | May 2026 | Medium | More systems-focused | +| SoCC | June 2026 | Medium | Cloud systems angle | + +#### ML Methods +| Venue | Deadline | Fit | Notes | +|-------|----------|-----|-------| +| NeurIPS | May 2026 | Excellent | Demo-conditioning as retrieval | +| ICML | Feb 2027 | Excellent | Method + experiments | +| ICLR | Oct 2026 | Good | Representation learning angle | + +#### HCI/Agents +| Venue | Deadline | Fit | Notes | +|-------|----------|-----|-------| +| CHI | Sept 2026 | Excellent | Human-AI, user study | +| UIST | April 2026 | Excellent | Demo interaction | +| IUI | Oct 2026 | Good | Intelligent interfaces | + +#### NLP/Multimodal +| Venue | Deadline | Fit | Notes | +|-------|----------|-----|-------| +| ACL | Feb 2027 | Good | Multimodal grounding | +| EMNLP | May 2026 | Good | VLM applications | +| NAACL | Dec 2026 | Good | Shorter, regional | + +--- + +### 5.2 Workshop Opportunities + +| Workshop | Conference | Typical Deadline | Focus | +|----------|-----------|------------------|-------| +| LLM Agents | ICML/NeurIPS | 2-3 months before | Agent architectures | +| Human-AI Interaction | CHI/IUI | Variable | Collaboration | +| AutoML | NeurIPS | September | Automation | +| Efficient ML | ICML/NeurIPS | Variable | Efficiency | + +--- + +## 6. Existing Drafts and Assets + +### 6.1 Paper Drafts + +| File | Location | Status | Content | +|------|----------|--------|---------| +| `omnimcp_whitepaper.tex` | `/Users/abrichr/oa/src/omnimcp/paper/` | Complete (whitepaper) | Spatial-temporal framework, 530 lines | +| `omnimcp_arxiv.tex` | `/Users/abrichr/oa/src/omnimcp/paper/` | Complete (arXiv format) | Full paper, 1056 lines, benchmarks pending | +| `omnimcp_whitepaper.pdf` | Same | Compiled | 2.7 MB | +| `omnimcp_arxiv.pdf` | Same | Compiled | 133 KB | + +### 6.2 Figures + +| Figure | Location | Description | +|--------|----------|-------------| +| `spatial-features.png` | `/Users/abrichr/oa/src/omnimcp/paper/` | Spatial feature understanding | +| `temporal-features.png` | Same | Temporal feature understanding | +| `api-generation.png` | Same | Internal API generation | +| `api-publication.png` | Same | External API (MCP) publication | + +### 6.3 Documentation + +| Document | Location | Relevance | +|----------|----------|-----------| +| `architecture-evolution.md` | `/Users/abrichr/oa/src/OpenAdapt/docs/` | Full architecture description | +| `waa_demo_experiment_design.md` | `/Users/abrichr/oa/src/openadapt-ml/docs/experiments/` | WAA experiment details | +| `waa-evaluator-integration.md` | `/Users/abrichr/oa/src/openadapt-evals/docs/research/` | Evaluation methodology | +| `CLAUDE.md` files | Various repos | Implementation details | + +### 6.4 Code Assets + +| Asset | Location | Description | +|-------|----------|-------------| +| openadapt-capture | GitHub | Recording package | +| openadapt-ml | GitHub | Training/inference | +| openadapt-evals | GitHub | Benchmarks | +| openadapt-retrieval | GitHub | Demo retrieval | +| openadapt-grounding | GitHub | UI localization | +| openadapt-viewer | GitHub | Visualization | + +--- + +## 7. Action Items + +### Immediate (This Week) + +- [ ] Complete 10 WAA demo documents +- [ ] Run WAA zero-shot baseline +- [ ] Run WAA demo-conditioned evaluation +- [ ] Update omnimcp_arxiv.tex with new results + +### Short-Term (Next 2 Weeks) + +- [ ] Write blog post announcing demo-conditioning results +- [ ] Create comparison figure (zero-shot vs demo-conditioned) +- [ ] Record demo video +- [ ] Finalize arXiv submission + +### Medium-Term (Next Month) + +- [ ] Implement retrieval accuracy metrics +- [ ] Run cross-domain transfer experiments +- [ ] Identify workshop submission targets +- [ ] Begin CHI/UIST demo preparation + +--- + +## 8. Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| WAA results don't match predictions | Medium | High | Focus on subset where demos help most | +| Retrieval accuracy insufficient | Low | Medium | Add reranking, increase demo library | +| Competition publishes first | Medium | Medium | Differentiate with open-source, modularity | +| Reviewer skepticism of accuracy claims | Medium | Medium | Multiple seeds, statistical tests | + +--- + +## 9. References + +### Key Citations for Paper + +1. **Windows Agent Arena** - Bonatti et al., 2024. Microsoft benchmark, SOTA 19.5%. +2. **OmniParser** - Chen et al., 2024. Vision-only UI parsing. +3. **Set-of-Mark** - Yang et al., 2023. Visual grounding via labels. +4. **Claude Computer Use** - Anthropic, 2024. Production VLM agent. +5. **UFO** - Microsoft, 2024. Windows agent architecture. +6. **Qwen-VL** - Alibaba, 2024. Open-source VLM. +7. **WebArena** - Zhou et al., 2023. Web automation benchmark. +8. **OSWorld** - Xie et al., 2024. Cross-platform benchmark. + +--- + +*Last updated: January 2026* diff --git a/docs/roadmap-priorities.md b/docs/roadmap-priorities.md new file mode 100644 index 000000000..26f86492c --- /dev/null +++ b/docs/roadmap-priorities.md @@ -0,0 +1,562 @@ +# OpenAdapt Roadmap - Priorities + +**Last Updated**: January 16, 2026 +**Version**: 1.1.0 +**Status**: Active Development + +--- + +## Executive Summary + +This document outlines the prioritized roadmap for OpenAdapt, focusing on ensuring the modular meta-package architecture is stable, functional, and delivers on the core promise: **Record -> Train -> Evaluate** GUI automation workflows. + +--- + +## Current State Assessment + +### PyPI Packages Published + +| Package | Version | Python | Status | +|---------|---------|--------|--------| +| `openadapt` | 1.0.0 (meta) | >=3.10 | Published | +| `openadapt-capture` | 0.1.0 | >=3.10 | Published | +| `openadapt-ml` | 0.2.0 | >=3.12 | Published | +| `openadapt-evals` | 0.1.0 | >=3.10 | Published | +| `openadapt-viewer` | 0.1.0 | >=3.10 | Published | +| `openadapt-grounding` | 0.1.0 | >=3.10 | Published | +| `openadapt-retrieval` | 0.1.0 | >=3.10 | Published | +| `openadapt-privacy` | 0.1.0 | >=3.10 | Published | + +**Note**: `openadapt-ml` requires Python 3.12+, which may cause compatibility issues with other packages requiring 3.10. + +### CI/Test Status + +- **Main repo**: CI runs on macOS and Ubuntu, Python 3.10/3.11/3.12 +- **Lint check**: `ruff check` and `ruff format --check` - **Currently Passing** +- **Tests verified**: + - `openadapt-grounding`: 53 tests passing + - `openadapt-retrieval`: 28 tests passing +- **Known issues**: PR #969 addresses ruff format, Docker build needs verification + +### Meta-Package Structure + +The `openadapt` meta-package v1.0.0 uses: +- Hatchling build system +- Lazy imports to avoid heavy dependencies +- Optional extras: `[capture]`, `[ml]`, `[evals]`, `[viewer]`, `[grounding]`, `[retrieval]`, `[privacy]`, `[core]`, `[all]` + +--- + +## Priority Definitions + +| Priority | Urgency | Timeframe | Description | +|----------|---------|-----------|-------------| +| **P0** | Critical | This week | Blockers preventing basic functionality | +| **P1** | High | 1-2 weeks | Core feature completion, essential for v1.0 | +| **P2** | Medium | This month | Important enhancements, user experience | +| **P3** | Lower | Backlog | Nice to have, future considerations | + +--- + +## P0 - Critical: Blocking Issues + +### 1. Fix CI - Ruff Format (PR #969) + +| Field | Value | +|-------|-------| +| **Status** | In Progress | +| **Effort** | Small (1-2 hours) | +| **Owner** | TBD | +| **PR** | #969 | +| **Branch** | `fix/ruff-format-config` | + +**Description**: The CI workflow runs `ruff format --check openadapt/` which may fail if code is not formatted. A fix branch exists with formatting applied. + +**Current State**: Local `ruff check` passes. Branch `fix/ruff-format-config` contains formatting fixes. + +**Next Actions**: +- [ ] Review and merge PR #969 +- [ ] Verify CI passes on all Python versions (3.10, 3.11, 3.12) +- [ ] Verify CI passes on all platforms (macOS, Ubuntu) + +**Files**: +- `.github/workflows/main.yml` +- `openadapt/config.py` +- `openadapt/cli.py` + +--- + +### 2. Fix Docker Build + +| Field | Value | +|-------|-------| +| **Status** | Needs Investigation | +| **Effort** | Medium (2-4 hours) | +| **Owner** | TBD | +| **Location** | `legacy/deploy/deploy/models/omniparser/Dockerfile` | + +**Description**: Docker build for OmniParser server may have issues. This is used for the grounding provider integration. + +**Next Actions**: +- [ ] Test `docker build` for OmniParser Dockerfile +- [ ] Verify CUDA/GPU support works correctly +- [ ] Test model download during build (huggingface-cli) +- [ ] Document any missing dependencies or configuration + +**Files**: +- `legacy/deploy/deploy/models/omniparser/Dockerfile` + +--- + +### 3. Verify Meta-Package Installs Correctly + +| Field | Value | +|-------|-------| +| **Status** | Needs Testing | +| **Effort** | Medium (2-4 hours) | +| **Owner** | TBD | + +**Description**: Critical compatibility issue - `openadapt-ml` requires Python 3.12+, but `openadapt-capture` and others require 3.10+. Need to verify `pip install openadapt[all]` works. + +**Test Matrix**: + +| Installation | Python 3.10 | Python 3.11 | Python 3.12 | +|-------------|-------------|-------------|-------------| +| `openadapt` | Test | Test | Test | +| `openadapt[capture]` | Test | Test | Test | +| `openadapt[ml]` | Expected Fail | Expected Fail | Test | +| `openadapt[core]` | Expected Fail | Expected Fail | Test | +| `openadapt[all]` | Expected Fail | Expected Fail | Test | + +**Next Actions**: +- [ ] Test `pip install openadapt[all]` on Python 3.12 +- [ ] Test `pip install openadapt[core]` on Python 3.12 +- [ ] Verify imports work: `python -c "from openadapt.cli import main"` +- [ ] Document minimum Python version clearly (3.12 if ml is needed) +- [ ] Consider downgrading `openadapt-ml` requirements to 3.10+ if feasible + +--- + +### 4. Basic Capture -> Train -> Eval Workflow + +| Field | Value | +|-------|-------| +| **Status** | Needs End-to-End Testing | +| **Effort** | Large (4-8 hours) | +| **Owner** | TBD | + +**Description**: The core value proposition requires this workflow to function: + +```bash +openadapt capture start --name my-task # 1. Record demo +openadapt train start --capture my-task # 2. Train model +openadapt eval run --checkpoint model.pt # 3. Evaluate +``` + +**CLI Commands to Test**: + +| Command | Status | Notes | +|---------|--------|-------| +| `openadapt capture start` | Needs Test | Requires macOS permissions | +| `openadapt capture list` | Needs Test | | +| `openadapt capture view ` | Needs Test | Generates HTML | +| `openadapt capture stop` | TODO | Uses Ctrl+C currently | +| `openadapt train start` | Needs Test | Requires openadapt-ml | +| `openadapt eval run --agent api-claude` | Needs Test | Requires API key | +| `openadapt eval mock --tasks 10` | Needs Test | Quick verification | + +**Next Actions**: +- [ ] Test `openadapt capture start` on macOS (permissions required) +- [ ] Test `openadapt capture list` shows recordings +- [ ] Test `openadapt capture view ` generates HTML +- [ ] Test `openadapt train start` with real capture data +- [ ] Test `openadapt eval run --agent api-claude` with API key +- [ ] Test `openadapt eval mock --tasks 10` for quick verification +- [ ] Document any failures and create issues + +**Known Blockers**: +- `capture stop` is TODO (uses Ctrl+C currently) +- macOS requires Accessibility + Screen Recording permissions + +--- + +## P1 - High: Core Features + +### 5. Complete Baseline Adapters + +| Field | Value | +|-------|-------| +| **Status** | Partially Implemented | +| **Effort** | Medium (4-8 hours) | +| **Owner** | TBD | +| **Package** | `openadapt-ml` | + +**Description**: API baseline adapters (Anthropic, OpenAI, Google) are implemented but need testing and validation. + +**Adapter Status**: + +| Provider | Adapter | Status | Notes | +|----------|---------|--------|-------| +| Anthropic | Claude | Implemented | Claude Computer Use patterns | +| OpenAI | GPT-4V | Implemented | Needs testing | +| Google | Gemini | Implemented | Needs testing | +| Qwen | Qwen3-VL | Implemented | Local model | + +**Next Actions**: +- [ ] Test Anthropic adapter with Claude API +- [ ] Test OpenAI adapter with GPT-4V +- [ ] Test Google adapter with Gemini +- [ ] Verify prompts follow SOTA patterns (Claude CU, UFO, OSWorld) +- [ ] Add error handling for rate limits and API failures +- [ ] Document adapter usage and configuration + +--- + +### 6. Demo Conditioning Integration in Evals + +| Field | Value | +|-------|-------| +| **Status** | Designed, Needs Integration | +| **Effort** | Medium (4-8 hours) | +| **Owner** | TBD | +| **Packages** | `openadapt-retrieval`, `openadapt-evals` | + +**Description**: Demo-conditioned prompting shows **33% -> 100% first-action accuracy improvement**. This is a key differentiator. + +**Architecture**: +``` +openadapt-retrieval (demo library) -> openadapt-ml (adapters) -> openadapt-evals (benchmark) +``` + +**Next Actions**: +- [ ] Integrate `openadapt-retrieval` with `openadapt-ml` adapters +- [ ] Add `--demo` flag to `openadapt eval run` +- [ ] Test with real demo library on WAA benchmark +- [ ] Document demo library format (JSON structure, screenshots) +- [ ] Add `--demo-library` option for multi-demo retrieval + +--- + +### 7. WAA Benchmark Validation + +| Field | Value | +|-------|-------| +| **Status** | Blocked on Azure VM Setup | +| **Effort** | Medium (4-8 hours) | +| **Owner** | TBD | +| **Package** | `openadapt-evals` | + +**Description**: Need to validate demo-conditioning claims on full Windows Agent Arena benchmark. This provides credibility for landing page claims. + +**Infrastructure Required**: +- Azure VM with nested virtualization (Windows 10/11) +- WAA server running +- API keys for Claude/GPT-4V + +**Target Metrics**: + +| Metric | Baseline (No Demo) | With Demo | Target | +|--------|-------------------|-----------|--------| +| First-action accuracy | ~33% | ~100% | Validate | +| Episode success rate | TBD | TBD | Measure | +| Average steps | TBD | TBD | Measure | + +**Next Actions**: +- [ ] Start Azure VM with WAA server (nested virtualization) +- [ ] Run `openadapt eval run --agent api-claude --server ` +- [ ] Record metrics: episode success rate, avg steps, failure modes +- [ ] Generate HTML report with `openadapt-viewer` +- [ ] Document results for landing page claims + +--- + +## P2 - Medium: Enhancements + +### 8. Safety Gate Implementation + +| Field | Value | +|-------|-------| +| **Status** | Design Phase | +| **Effort** | Medium (4-8 hours) | +| **Owner** | TBD | +| **Package** | `openadapt-ml` | + +**Description**: Implement safety gates to prevent harmful or unintended actions during agent execution. + +**Safety Categories**: +1. **Pre-action validation**: Check action against allowed patterns +2. **Dangerous action detection**: Block destructive file ops, system commands +3. **Human-in-the-loop confirmation**: Require approval for certain actions +4. **Rollback capability**: Undo recent actions if needed + +**Next Actions**: +- [ ] Design safety gate API interface +- [ ] Implement pre-action validation hooks +- [ ] Add dangerous action detection (rm, format, delete, etc.) +- [ ] Add optional human confirmation prompts +- [ ] Document safety configuration options + +--- + +### 9. Grounding Provider Improvements + +| Field | Value | +|-------|-------| +| **Status** | Package Published (53 tests passing) | +| **Effort** | Medium (4-6 hours) | +| **Owner** | TBD | +| **Package** | `openadapt-grounding` | + +**Description**: `openadapt-grounding` provides UI element localization for improved click accuracy. Needs integration with ML package. + +**Available Providers**: + +| Provider | Backend | Status | GPU Required | +|----------|---------|--------|--------------| +| OmniGrounder | OmniParser | Working | Yes (CUDA) | +| GeminiGrounder | Gemini API | Working | No | +| SoMGrounder | Set-of-Marks | Working | Yes | + +**Next Actions**: +- [ ] Integrate with `openadapt-ml` action replay +- [ ] Test OmniGrounder with recorded captures +- [ ] Test GeminiGrounder with API key +- [ ] Add grounding visualization to `openadapt-viewer` +- [ ] Document grounding provider selection +- [ ] Fix Docker build for OmniParser server + +--- + +### 10. Viewer Dashboard Features + +| Field | Value | +|-------|-------| +| **Status** | Basic HTML Generation Works | +| **Effort** | Medium (4-8 hours) | +| **Owner** | TBD | +| **Package** | `openadapt-viewer` | + +**Description**: `openadapt-viewer` generates HTML but could be enhanced for better debugging and analysis. + +**Requested Features**: + +| Feature | Priority | Complexity | +|---------|----------|------------| +| Video playback from screenshots | High | Medium | +| Action timeline with seek | High | Medium | +| Side-by-side comparison view | Medium | Low | +| Filtering by action type | Medium | Low | +| Benchmark result integration | Medium | Medium | +| Failure analysis tools | Medium | High | + +**Next Actions**: +- [ ] Add video playback (from captured screenshots) +- [ ] Add action timeline with seek +- [ ] Add side-by-side comparison view +- [ ] Add filtering by action type +- [ ] Integrate with benchmark results for failure analysis + +--- + +## P3 - Lower: Nice to Have + +### 11. Telemetry (GlitchTip) + +| Field | Value | +|-------|-------| +| **Status** | Design Doc Complete | +| **Effort** | Large (1-2 weeks) | +| **Owner** | TBD | +| **Design Doc** | `docs/design/telemetry-design.md` | + +**Description**: Create `openadapt-telemetry` package for unified error tracking and usage analytics across all packages. + +**Key Features**: +- GlitchTip/Sentry SDK integration +- Privacy filtering (path sanitization, PII scrubbing) +- Internal user tagging (CI detection, dev mode) +- Opt-out mechanisms (DO_NOT_TRACK env var) + +**Next Actions**: +- [ ] Create `openadapt-telemetry` package scaffold +- [ ] Implement Sentry/GlitchTip integration +- [ ] Add privacy filtering (path sanitization, PII scrubbing) +- [ ] Add internal user tagging (CI detection, dev mode) +- [ ] Create opt-out mechanisms (DO_NOT_TRACK env var) +- [ ] Integrate with openadapt-evals as pilot + +--- + +### 12. Additional Benchmarks (WebArena, OSWorld) + +| Field | Value | +|-------|-------| +| **Status** | Future Consideration | +| **Effort** | Large (2-4 weeks) | +| **Owner** | TBD | +| **Package** | `openadapt-evals` | + +**Description**: Expand evaluation infrastructure beyond WAA. + +**Target Benchmarks**: + +| Benchmark | Type | Status | Priority | +|-----------|------|--------|----------| +| Windows Agent Arena (WAA) | Desktop | In Progress | High | +| WebArena | Web Browser | Not Started | Medium | +| OSWorld | Cross-Platform | Not Started | Medium | +| MiniWoB++ | Synthetic | Not Started | Low | + +**Next Actions**: +- [ ] Implement WebArena adapter for browser automation +- [ ] Implement OSWorld adapter for cross-platform desktop +- [ ] Create unified metrics across benchmarks +- [ ] Add benchmark comparison view + +--- + +### 13. Documentation Site (docs.openadapt.ai) + +| Field | Value | +|-------|-------| +| **Status** | MkDocs Configured, Needs Deployment | +| **Effort** | Medium (4-6 hours) | +| **Owner** | TBD | +| **Config** | `mkdocs.yml` | + +**Description**: Documentation site using MkDocs with existing markdown files. + +**Existing Documentation**: +- `docs/index.md` - Home page +- `docs/architecture.md` - System architecture +- `docs/cli.md` - CLI reference +- `docs/packages/*.md` - Package documentation +- `docs/getting-started/*.md` - Installation, quickstart, permissions + +**Next Actions**: +- [ ] Verify `mkdocs.yml` configuration +- [ ] Run `mkdocs build` and test locally +- [ ] Set up GitHub Actions for auto-deploy to GitHub Pages +- [ ] Configure CNAME for docs.openadapt.ai +- [ ] Add API reference (auto-generated from docstrings) +- [ ] Write getting-started tutorial (5-minute quickstart) + +--- + +## Dependency Graph + +``` +P0: Fix CI (PR #969) ─────────────────────────────────────────────────┐ +P0: Docker Build ─────────────────────────────────────────────────────┤ +P0: Verify Meta-Package ──────────────────────────────────────────────┤ +P0: Basic Workflow ───────────────────────────────────────────────────┤ + │ + v +P1: Baseline Adapters ────────────────────────────────────────────────┤ +P1: Demo Conditioning ────────────────────────────────────────────────┤ +P1: WAA Benchmark ────────────────────────────────────────────────────┘ + │ + v +P2: Safety Gates ─────────────────────────────────────────────────────┐ +P2: Grounding Improvements ───────────────────────────────────────────┤ +P2: Viewer Dashboard ─────────────────────────────────────────────────┘ + │ + v +P3: Telemetry (GlitchTip) ────────────────────────────────────────────┐ +P3: Additional Benchmarks ────────────────────────────────────────────┤ +P3: Documentation Site ───────────────────────────────────────────────┘ +``` + +--- + +## Technical Debt + +### Known Issues + +| Issue | Severity | Package | Notes | +|-------|----------|---------|-------| +| Python version mismatch | Medium | `openadapt-ml` | Requires 3.12+, others 3.10+ | +| `capture stop` TODO | Low | `openadapt` CLI | Uses Ctrl+C instead of signal/file | +| `release-and-publish.yml` uses hatchling | Low | Main repo | Aligned with meta-package | +| Legacy code | Low | `/legacy/` | Many TODOs, not blocking v1.0 | + +### Code Quality + +| Package | TODOs | Notes | +|---------|-------|-------| +| `openadapt/cli.py` | 1 | Implement stop via signal/file | +| `legacy/` | 100+ | Historical, not blocking v1.0 | + +--- + +## Success Criteria + +### P0 Complete (This Week) + +- [ ] CI passes on all matrix combinations (Python 3.10/3.11/3.12, macOS/Ubuntu) +- [ ] PR #969 merged +- [ ] Docker build succeeds for OmniParser +- [ ] `pip install openadapt[core]` works on Python 3.12 +- [ ] Basic capture/eval workflow demonstrated + +### P1 Complete (1-2 Weeks) + +- [ ] API agents (Claude, GPT-4V) working with demo conditioning +- [ ] WAA baseline established with metrics +- [ ] First-action accuracy validated (33% -> 100% with demo) + +### P2 Complete (This Month) + +- [ ] Safety gates implemented and documented +- [ ] Grounding improving action accuracy +- [ ] Viewer dashboard with video playback + +### P3 Complete (Backlog) + +- [ ] Telemetry package published +- [ ] docs.openadapt.ai live +- [ ] Additional benchmarks integrated + +--- + +## Resources Required + +| Resource | Purpose | Status | +|----------|---------|--------| +| Azure credits | WAA benchmark VM | Needed | +| Anthropic API key | Claude testing | Available | +| OpenAI API key | GPT-4V testing | Needed | +| Google API key | Gemini testing | Needed | +| Test machines | Windows 10/11, Ubuntu 22.04/24.04 | Needed | +| DNS access | docs.openadapt.ai CNAME | Needed | + +--- + +## Appendix: Quick Reference + +### PyPI Package URLs + +- https://pypi.org/project/openadapt/ +- https://pypi.org/project/openadapt-capture/ +- https://pypi.org/project/openadapt-ml/ +- https://pypi.org/project/openadapt-evals/ +- https://pypi.org/project/openadapt-viewer/ +- https://pypi.org/project/openadapt-grounding/ +- https://pypi.org/project/openadapt-retrieval/ +- https://pypi.org/project/openadapt-privacy/ + +### GitHub Repositories + +- Main: https://github.com/OpenAdaptAI/openadapt +- Sub-packages: https://github.com/OpenAdaptAI/openadapt-{capture,ml,evals,viewer,grounding,retrieval,privacy} + +### Related Documents + +- Architecture: `/docs/architecture.md` +- Telemetry Design: `/docs/design/telemetry-design.md` +- Landing Page Strategy: `/docs/design/landing-page-strategy.md` +- Legacy Freeze: `/docs/legacy/freeze.md` + +--- + +*This roadmap is a living document. Update as priorities shift based on user feedback and technical discoveries.* From 41acb564ad245d9a00eaf01ca77a3eb36420e3e3 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Fri, 16 Jan 2026 23:53:02 -0500 Subject: [PATCH 2/4] docs: Add open source models to architecture diagram Model Layer now shows two categories: - API Models: Claude, GPT-4o, Gemini - Open Source / Fine-tuned: Qwen3-VL, UI-TARS, OpenCUA Co-Authored-By: Claude Sonnet 4.5 --- README.md | 18 +++++++++++++----- docs/assets/architecture-diagram.png | Bin 108465 -> 97211 bytes 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c1e34f762..fdb66a053 100644 --- a/README.md +++ b/README.md @@ -212,11 +212,19 @@ flowchart TB %% MODEL LAYER %% ═══════════════════════════════════════════════════════════════════════ subgraph Models["Model Layer (VLMs)"] - direction LR - CLAUDE["Claude"] - GPT["GPT-4o"] - GEMINI["Gemini"] - QWEN["Qwen-VL"] + direction TB + subgraph APIModels["API Models"] + direction LR + CLAUDE["Claude"] + GPT["GPT-4o"] + GEMINI["Gemini"] + end + subgraph OpenSource["Open Source / Fine-tuned"] + direction LR + QWEN3["Qwen3-VL"] + UITARS["UI-TARS"] + OPENCUA["OpenCUA"] + end end %% ═══════════════════════════════════════════════════════════════════════ diff --git a/docs/assets/architecture-diagram.png b/docs/assets/architecture-diagram.png index 1232c988d5e4cc2ca846496cd46c0bc04614ffc8..2a8ebf1d3994d793f09353a122c27244db457ddd 100644 GIT binary patch literal 97211 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 literal 108465 zcmeFZ2UJsA)GnH%M?LBhjz^sXy*I|YQN2kNgf<4a$30kAB@aYSVptIYtCbb0yrY7>}ltCT*xuQND?Mi}0;OY2i zQ8<(1AI5@;=Z1rV;XcU%xBot)ZPDGLkkXMFFe@^ZUPIXJkW@Ydp8NmE>rJ|bHna)Y z1=j>Y$1zdahPUS0Ex&;`CMPg@`R%un)B~^l>roF(-^L#X?_SqZQfjpfW70jsmhx~x zk>ezI?@2JGqGI~*)>Z~al@ar|Ef;@-KEJPaR=}Xpx4=MXw6>0V*6Rv*4|ec%fB$H1 zRxU5|p4%Iyz0=Tp(CedY=*r(px>&AL>)rK1mH*ub*u*Z&6QDS1xfYp9X3;AdL)pL~jUQLD2t-%@(W+qmD z^>SOm!7>{*&qcjB+s@?Zl|3G<168p6#h&*``Nv;j8ZC03Kfk-XGeBT-4ZkFEjmgV1%Ff9-xAQhp$g+2#Ut3iZmpn)K{RoX4;s4{c9NM@Xy|kp7Z{L5EReb5a zS$l3y&Mfu3-)SbMf|JY#IPw|_g~Currl$)eBKX9_^tH81bH1L0U11G>@^cli>X)I) ziV^IRw=4tKLK1`~vW{^%WyZyw6*Q|>J$bIMB>9m2$0x=92fHnLRZc7joe5kf6AOzV z!hf^ZFk&x==9LpCX5P0nEYE-GN1?WIf6ggUmoXK64B4%z8m(VYIMt#>+TTr0qYp!8 zsGAd5Bxt%H;VdUdp=^zI=Q-&6Ao}{UNn*|;SpS7=UNLJGp-i)T_ueXnzkS!0W@^7U zkXZhM?Fu4Ua>H38c{BUr!yhw~_c5o~a0zMVd@o*b^ec098iEgKM`5vv7QPQkSjEr+ zHX2-FUW*bVrI$aVJD`I|lfrW&N=xT9zcS0uZ%=IVUA(B>)n!BZcvu5k@9=PZ?RUw; zFtW_Qa`~pXF}tmXx}y+_-CV~) zbgh?T=0yeneAfxh<^v@4&e_v$27)G{dUIVx;m$&h``zeP(_*n~CKH#BT^h(zUSr0|^h<7Qqcr@ywF+Z;DyVPOH46&@ZgV;S30vzO0+Rv8oWm#%?Su$l9g!VwX&J7UX@GvuD$}hE@snRA#zka zXrg;g-g-+!`sAFuYV>|Coh?f<&4ktFoS;e3+qb@uITaeawhxMzQ|;Q~Ff()5+Z(fj z1QI6dN{`q=e{LqxZ?3U<1%YVYn+(Cg5mU_(15J$bkCwt-zczevB8jjHwyJyQ&Iitb z!qDb$KeMW;Dpd^){N}<*O23w)m=C#ouaVAHkS2YY8*zp=a*(!7&{k4*AFEa`Wrnvo9S=|ebV20Y(m z&OmAXHtHzD5r5)Y3mjcpX=zNZ-OQmDVEiWkq2w=Y_AW3+ zy-`vQx<7eQBlYH}(bUDdCDNTS6+OK|Vt{Ma*78ZddmKPoi-c}N*?_9pj0q*={#u~( zNQJ5M=w6;(_m-5~kMOu6k%`Yhec*hTemFg4?#9s%qzoD}WRm;}l_Kh`t*l~6j+z+< zb0)I%eQHfYX`%hvVu9Ae&YH?D^;m&j_nhdXdp?k>-rDj$@`f39uR`V4FaI?CS(xKq z1Z&bsg$yY#11))4F&yY``r=UO3$!7o@P<{+e9!w#KePxpx1v>xCO+o!X@?11lK(dO zg6j(X(ZRlDh3{H&;8(*BcaO{NJz@mj)P8Ssh3UMn3fPXsR$X;mIX@Ry@TZ+!|JG+I zd=~Z9_!UD;j*2lNhEy63ck{khz^7}`Rv8|mVEgR7YP_mR+4Pwrqf+B?J9-wk+MxY5 z`S9KWm5*OH$MfmKqp-6a|AsiiE8xI)H-cl}ltuh)6lCY{g;rkb!VBh?=WRIHm$FHo ziJQ>`P2%&*pfMyp5Kn{YCF2UNetrXh<`ucmzVh4Xc>G{{9GI z@Mfur_1^0DGjGD$&D5Ikc!WmsKr9@XK+@b>7|e%3>6aEg2_Ww8Ha_Z1UViiXwM3Pe z!*upGF@ShJzNf+-2_l`&#w_ha7_(6OU{$q2;gD1Kg`v7_EqqY`u*ZPyDw8Uw5Zex; ztku<6)X(_nPZ8dxQjaq54*FK3W&gu=_sz8_y5E4-Jz)1-a0i9~uY4tb2}ugCWo7`9l&3p3!Yqf~kK z$0dZdsa$*`J ztGj0rKMH@{%LrQ)`ufK&!1lPiO>Rb8UA^7`zpjUMao+5b?zSh7y@vK&N`HDRd4AaL z7!*lQG8Yd30p}@(y-b!5E(3>0S;n?Ul$~NbP)1U{QSc(mMqLtNMbCE!QBx}g4A2eQ zUR%7WSin{kzd?u6NoGy`kfW8Aj)Wi%S^ve_wFIG+tH+K7DBexQM{@k8s-mLT;JA>AU+bZ9!ec2G;GqAZZHT_*%yUu%=@a%}d&j?)afVS=A*BKmPXiv~E1UTmG zIdAm|B=!#U>Cz7cdt1xa`?9_PbY;rocy7D^)`7C;T|S?0(Im?Gbe8;G?vX1#n=3jRksxSC#EwASDH@l~xhQ1Z5m zOBcUBI;&LnCq{E~_waz=jTLnw*fJp`Wc(Dfm!^h>zSEMQIWF~`YW&#ryf5u*lCF-< zSq=`&2_`eWGV4~ec57?vE8<(JAj-HH8CsatdMYvtS+u!X40CgH3tS`bpR%g;T|+rJ zIRPw}Wcub9ELvK=@e|*|LhCY@pEaHQS%yLPI)wINu;~zBZuoD1ggM4Cwk3baGm~s# zA9Z$i_9(h6#6boD!QDqFBiBeR?n(^Ft*pGXRQtt=Vn6hXEjx$Wgy`!Bc7}^NH)gy$ zVKBO!`ltU$2Go`R!4=>UZzKO(k_7k#XgSd9$%0du)XyuQm_PY4^Q>Slm)WM|eI!T3 z$!-;1uw5Gup)ry5C%^96-@%fYGf<$&apW=ereR5Mw8Y9m>k(Xlym?|*zu@bD#Iz7m zclWgq44FNGw5#SfavzbNu=qz_$zeV9?N0O)=sEY%gEaou{D-X{7!F_Y)NVQBxqmBA zJJfAdblJj!NoF}r>d(n4X&-UpW8z=WdY#8AD!q(g^?s}!PmjB{c<)JYrL(-!o3*=1 zzhc>SeHJ^@cmb_LhkP0vr^rR$_37XNY)2x$3H;Wc@Vsg8O~eP(!E zUi0kF&4Rziy#J3<_%}3yAN0pO`S*8VIhX&*fWSNdV`Bba{(#3}!otFFF)`W$4`5dg z(G#lZuGLyJwug5QhR%8Y-0;1(vt4P$;o+XAxDK9y9PmNq3c#f%jX^JDWF64~+d+!w ztR5WybCUpa2v3SbnU1cmewCA?eu06m*W!TT@`!~Z2j^oo9UUFuhTWGZ8V%>VTiS<* z^??V{?@m8FJv|+0YV|rQ3P1U2T)(a=(Y9;q`~gEt^JuqE&)D1 zZ9YCe6r7P!3%a{Taz%8Ag8i9GM^D%e7TT+-tKV~XM}f!B{P>aMfOlvkU_(_Cujvka zw{H&5H)-%s;?YPhJ|{SG*6XIk=728EbIU1ff4>T?&e9^&z#}K8*`9#R$SsRt6>H3P zymxk6=d9c>fI)T$3QD3s0cgz51AgH{X1Bxc`d8osm(7!&-1J^587y3D_p-!TFgot6 z_WInNI~8r2pP$bvL6&^+oV{cO7qGp8)^9K9k@~Ia^PLJug7_&exeT zkacIdxC+u`k4+EJM_n6%zq0}F_o@kLRL@Km8LjCSkB*P=&*Kdzbf=m-H_YccyMFr? z1_I1%Es|KDlq?%i%5PZgK5ErQp4h1f3VL`zT{&rj3v^3w8yOy+=%Zn;le4e*|5EMA zYjdPgg~Lbx#J@N>cb1bAy}My&51uIHF&meoo}>?Q{M{-fRSB#;IySbvIVuBP>$O5y z=;y8U+b|Jx7%03KnG_TA{)W}dRwD7D@}Upc_bG(i4ODWw1EJWXIBi?jf71|buGULL z!cWS1#IOvDHME}Qa~>+udHVDK$fa>_I@WdI-R;akyjQsgY~5<@hgVs7IU(lqgJcQU zoX?-{Y|^$pK)QHLkz>TXbG+e=-0n16fitFFSO*1SBUipzO|VjGO^tr_^4j;V7S^9s z$Ngt--(CR}gOPKcKLy>DDp@ozFhIgx11EdqhPK32tC+aBth_uCRW-GCL#LT`!vW+r zJ|;>_O?Gcy*Q8|9aClVu%Jm*gQ|y6vTJ=LFE-s(`T3uaTuh|ZJtC4eI0*2QFcz70H zY8jhYq@<<>HVH$wjX1oWa9Q~P1@~GWQ6>`Q_>If590m)Ev@@IY^zwDEdOqsvan~$y zo@!o3#8C-Hf}gWpO}fB~P6Zpx-S=X&8vwGi)S~eb5 zc6PZ(Pq7-)O0hoCaD35dk3&GVYWzl^<*TSD-G>h!KAjRSU^pxH3+dewXOtV#Lv4<_ zQVXl1xa3WK`z>G-Z>Xl%u!jV&5iRa&QE>cBj{Qi*aV0wi;064@zmTA`3&}f>1}V*? zjG#U4Q|GVWZA-2T8ZLWbQfB@1?=$p;!1-a1C+Yr+ejxpghdWQ;5V5>b{l~Zh--BeV zZ6Y{Vd#jSbgZ7w9KO&{<AmGz)$udTm4Cs`u+mWZvo*y3`}`vOj+XSHp|+YcefLf{BZ%`R36moogO zAoI0ULO6?Vg~!}EE7PWza;(x@&VIgn@nurBjk~iamx;8xzt5ZjuB*_tle1^Ocgot% z&hXtv^LXY%ML@IcZq)cZ{`&Rnqi@li?(Xh`9~4Q}j#RDfme$yo&CUL(Z~f&jaR+;L zr`QG!_0f8|gkt38LT+rh_;42DWW&K8)!o}0pusrK;QB;3fHR+}wKkU|ZwcXca_YdT|B8<16E{}>tNw|zzdoAs}1Nmt|x!q-M z1g3tyxYm2wd7u$V<>^e3Xc`hiERT2%ev7dRCnXq)i;J^LdB#CHK zNAnqwJw6LL99~8ybNX)$8xEA&NF!<&B)@>&0oOB9?^mQ9IwSQbtEl?dH)|bNi&r)m z&Y<9283_iXu_zQ|$Keai@f0HajUS4Y&LjaFQ@i`XP1d^4lqFm>z8{C2f8QeNK7GC* zK*?fs0Jy?U;Okjt90*%|DXy9~C2*G%(A zWz~4di_8`X85F)1cQF%0IN;F@$zqOz2$S+=t%)G%rkgo7Sxvz2=QSQk<$);ht*J>D zwb`q@ve=Q-e+0(~B2stlwfD_cieb3k+zib=;JERH#*Si+gC?(EA2%tvUuE05*Rx1D zIFJ^#YSsge#2B1y0^E7D>UOsrH4lU=v|aaa?j8m2PBM2n{`g`T!P|D`%o)o5h61GH zc+5&s3sFpI zFW`wD={L0bs2UEJ^g12b1BF~C#I~KClBNK>Byd#%64(=f=tu!tCtTcA z)+;nNB&k>h%qN`h&!Z263G zzD+9L#!4d{DH6JB30I>ru8d;#eRoiskjtZPMJT21+UtS{qMsxjxk#j4gklZQ72TYA zd-fFroP>(by&gG5C8az>>^Y^;6DLM;eI}5Sge}>i{YT?ImK=Z+R6Bf3E`~HWmuMMO zI5`R1-KiKTc|dQ-Zp^43W4@z69{?VUiIX!2(mQb4Qoh$!3^B8E$(KQxisflQfBEve z?C!M3zlv@36TLqP_)Nmcn_i)8Eo{6>4?ghXbJW~ow(tcj-<>--J%hi6618dbg*55u z(*O{&FEa^Qm`ycnBosLM)zsD^jKfAFSwz%HgR~F5nG+@cdu2f9lZ9;(+Tz?Q&_WmP zWBCm^jowwaCt|MH$aW%k*SWtoD>;7u49LTcIn4EQ6d8}%igbT;#vq2S4_RZSgII(V z(ccl>d;G^trHaQ~5Dzb}U1xfO(M+2ppBP2#6ic5u0 zo6{lOwGk$i^Qe^*R@^8AgRR$UMA@siCMQpUM?ir z9km~=It4`r5UlY1T8&vV?ab{&;%(qWbc)@;0ncxAGoN3!{{FcJC{PV>l@WleC;$#) zIzJ6xyqq0GRH-!Dg7Ro9l8UlG!Ks_?c!S+QE=jQxTCsBGA!5r$CWJiz{d+ zo1I-%&A4@qPq*0-U}vRg+Q!RXdz-sx3z{4s>EaV1+}jW1b-{+Yn&TVeIsO!Xqi%V6 zEkxc!s3(}taDtJsAH=%C994mD+eZsrNjlb z1XYElPQ&y4HpF!XD;ViOM*wYtZhXG$XVKeUX5a1g=gg%foOrlbLe>&)0^+oTVXFS zHMfXgi3Ml}M6Pww(0avq{&WL`R6BK|&&@L0JHfEd+c_3a0qz!^smF8mPsb!jqB|Eq z;9nsqq^)fSg@7UxN_osqX{w`zC~#Y?tq92XK?#`EUcr92W>dpd5jMx6FKYo(crmB> zOur3Vhozwh%_DGWZzojQh@t1EaVFAO=_FXY5z@sV|A#%CD{k52D*s}*-Vg$@TXfvy_rSA9^~;5rtZB?8yvMoBe~ay(U8{UeXn7bAlsGHC|Xc zS{(ex;)-oHuZu{2qNTogsP{}JPgUcYJ{r2q@`$?g>(Cpa0vL2@g zl_H2YuWs@n_PT>v$10qH6cufVxntuZ%1&@zUQz)@4xcO*B-u90`=H!?Mh5^skrr7Y zd9O(#&=|)lk9xI43szycN8IQ43b)s{T+lr|^4th0oZ~>WVlzfoZ?b}rMxbEcuh4I>LOY8wKcoz8-JaOIKa&_I| z9Rw&&G6!Wx$@v?GF$Wjiup*+c*eLy7aM)JJP@w$T+1aZ6@ffMcbN`f-?^+TH{_O?8 z8iSM(5U~9^gR${NrapRjG+yk%=kpwLf%7>StjrN#fh}@p>JJD@$Km-h3u7)3t}WVY zH~1HsVVR$2dwjh_efSxu0@|$R&1{G92MWpX(Whs_dlUjUs4ik{yUf1nMt*y*7$%;4 zb4?_IMWo}rVrHSHDijcGBQhYb2HZaiF1vcG<!V3Fai5DcGu?{-e|?@%ap#39 zDhv2672r8Q-K25=yOO!SW)F%{e-&tUrVN+aqye1H13)QA_e}t_iU(|kRjKQw640p` zkb(yVg-ruZIK{SiK5f+%w;K#lf9s6CxVsO=ef%p4qi^Jlz5sX=knmc0fq+oyfVVKO z+XD4PBFL9;2||{sB*!<*LP1SZ)NQUp5Pe7+-SR!4(&n-&v*B-f9sqh1EcqRP&g~Py zp%%e3mWI{OHz|vh5`9TPqCl_$O0l4b6ilTZh5?- zC(=CoP44HWrn5{;d?0>X1*m)r;wxzS)Mx(?WSSuM>DPIiZ#J`agL-x~fG_nfErMic zfa?>>W^Efi7hFMD)dzBgFbDwN5LkwyJfXOpoumES&P{-PRW&umbI!F!J*yCD-_(|v zS};}sR$6S;!cd@*qPZKn?4KZL`e?wiAR!T;)JNmBOsFhI`BW?Q8rvk>X5k~9om~$A z^MT3&^uJLj3*$5%_;fhp_;Eu}NXy)#GC)dQh4F?4~L{kCSbA82~bU-^6-w?vcqf@!d?!egN7)8i)?cbJ}6F{ z?}r;t+vq7-7|&K(0C;_)@-Be9-sG{XHYpobd|3`(sXF!h^t4m_{doXd5q$t+awBj!*%O}XY;0^+vPS*GMHAAaE+#k0@<%n>)_xZKjI!bE^TciBlxH) zVXV=`lK}`l%SpElA)M2LP**QW_CH!}U}W^_&|%PkL0J@wD?{P{wr=tb<|73?Nb2I1 z?{Lw@UZQRvRa^hQh%Y#C>Qq8mL~$D^0SOZ-&dW-9cz#Zm%(x=8=k#={orJu2`LY6_ zD8jdB-BOY}b8UI1qmz^ybms09@&2KLYH!;Q@4TMR0pWP0=mz2 z^MHyeuxfEpRjIxjOi#<USOCCX$7GA=zGLU?K@ zKx4SI-8M zlg|P{eX&y_BN1dTfL!VwDs=^^?ykL^iwhHkw2vI!BLUnCnqUlo+Tfob?{_a69UWCR zuiflxjwk@!;H}-gt!N-r4jJED#~6<;#A@NR0RnYlodWEx9$JrYDhQ6~mfc1ti?y5G zB=nXRE)L}9Hia_UgQRpIPg~^0%LDsD`_rtfPT{v|dL>0&iAoUG;>}Qx1B*(NX0{7| zbI;(|M#OAMOT!#(+cGvSt>K|nX87|vu{?rMiizlZ@|!}X^QbEXLUpw$x*$!1%o&u& z1c0XjjSeZ8Bg35zy0#@UyIdehYssg)E*^3lyF)_LZ`-^337Qj zn)gfK%5xC0asu}P{ThVs-eo83tYt!MHLLjM4U`HvXt0j7Yv#3YGsz4bwGRC`_`Rtp zbCc2^yr8&A_ac2;*`3<@73L<>qetac1=CiBw@$k6D> zQL;90B2!|!NjwW91X0(6C+WUFjFEI2yOn=zBu5?SeYx$w(K%NM5*PpJ7~Y6nc8sh6 z(Q3yo7$}*;KwkDD-Z9^8YId8Tj_ZMVpAg^I{H#Qa)sR{VH`9We6vNtOCkixz!RnD!WLPxl5y< z7MN*XSH*Yr>X-Se@*cCBUK5E4ThYjCTl*E|ycvO-Es-1s*4EE&+~McrLwI|8*AS02 zMP&6uE{=%awtPjL40HPtk*gSyEkr}kZ-Qhxt8cqaJUknzRzPAk8)oxRrmEG*IYL49{ zkg}>OvX_>{gq#;`w;4om64lk!5Pk-!as672QLd(J=iAbI<+jK?`GcWskm7)%=*TYG z(wOn66JS;Vl~V!g27oI<;->UCS55*I(+y=Xhp^-K?+*a0XaI^Qo2yMxY7#qjyDPz# z1fc8v1WPRfbw#W30Y~-5S@M?;P%u}Ynj`Ro4zWamU;TsxvAGCQFIFAJVoqR%3nVDfm4S?(N13twOq?S1{`dn;mf*`0!O4f|^eJ=8|w*!$4bP?z*^x1$a zYK_c96bO6q2?;#R%-B1|cO`&F2g?9;1H<96negSg?hM^l6n9aML2a)j;Iwb^@fpr6 znh=MAX4-7Hd3l+4kv*68tUq|~ruBYn^$9k$Tt~cg5ck&ASdm^yPvJ_~Hmh;hG+6m+ zsy*b)FLZSd)RB0k>w1KsIKs&j_6J9eBp#bv8%8JhA{u z{}cb{@tk)fb@!AeLQBhq{_Qf=hk4fRS2_x-DJd@(nGF!Sy0x`chQ*+)sA)Y52^seM zC(w6a|1Ftbh%?w?`167^5CC9RTJiYhTAPOtKU9E143JPe0J-(`tZ&_yz+;njv@AxF zii(ysMiDM%=IzYa(ev^4~4bDxIj)nE6WS zB55M}<|w#(pe?~F;_WlxOolPee`=l|&3OSOm8r}K0>53dsJ;LD2wMO{YW^*ElBIw# z0@oG`!VBOXMyL1muEAhe%>Lu!xu6W6eEormcIa=QGj+Mk^gY!2`t!9<`u}DD6>RSR zR08g*%KN$J$3DZQOW|*ruzZWx$$UWhQE)c4V(M~S!lmKFYl~N0G8zgp_3D3LxiJ>d zrz--tfy&x1A~aqyI7RAyi`@CZaAe0&ZAUu7j$ZyxjXarCi%HjCiX#p?$|zrmiq$m+ z-A?cG6l&(;u6nuFPawIIqGxns9MFPcd(zlgnD9YiOCDT{UKmV)-rpm!7cqGWt`3VV?#4^xqLg|@2OkA&JYH> z_2A#;-~a2r|GPU3{+E}&|EKK0&UD~sm&dmh&|4Z_Qo>}ihhJYZd0oAAdw9rt2=r(TfhO*5 zZ7I*1yeVKM*k4YH%r7qf)ycGq-Er!B@8=v~OfHF&lR{AI*--}e&s0vj$?@Z*Cgrn@ ziX32E<@#yITaXR?eEqkz0V(D|HaWHiQfUCHgP=hO85w?}j`@FW`obeFLyi`ZottZk z#XBDj`+7SQtZQ_q~SQGQGxU=M>N;p_-!`Tdf`mlQA&(xq}MaC;ns z)^572eCP*2KYL5%E1+K7B>DGW#~7Y3er3+Ra`EDg(W>i+KG5Yz`E*REt+UhkXHUMz z+(99rR~`+Ogt~tz18uNJerMOCADB=7_>na~e-k8MW%}c&a7VvsmFMS#EIgVv$^l1I z+Lcz92X;0$g!NqT2PNxFEYZLDl~`Rxg=l9QwR~*6o$dImeNJha{pnNfSl5YgsF3!N zjnj{z&5NRND%8uC2_Ygr!^Z`sb*bf$dh5A)B)86zI8lQ2GqyGq7 z0}Yz(r468URmT8zQCQd{CZ>0fy3)>XS{1HsJAn&9Wr3-d0)mp_SBL3uK3Rp+ElrF* z@tIckFJ5x?Pq$jyPvHJ)pbXCKEf#DST1a@zK7_jXL0^5%cgY|cMHsZiw6)~ops@jv z9=!uMd-42vdux87l;@*jlS-M0^;}Iwkbe)mgAQ!K*5c8{$zlZ#b6vOi1TLL|NDX+ysw1HRmc47^K-FieceATFMKphBB$zG$0fgf!-BN>x8^fBdtmo zl!ThmftgAcKsJpoNad(>;UHqAV=|g+fkrz(5gkzM750a% zp1({$JVg%9p}3B%DXA_ESN@0NAk_H23jy+(%Vabr8i($}04N7wd!Rk5c6F2#4fW%a<>X zU>?Kue5{RUGiY_6qXw>L-Z^53(7F31iJkfo*j<0l_&R7zhtt;EXW5GLfeU(4ckI_h zm`_$DM~dU_dN;n-YiiWHW43c85f0R+y>tci^fzxRg7K0!VbZRZ^wsaN&E2C_%lLSK z<4Wekg!?-r%o!ADn*v?~j7;4~o-_dyMcIW5uge@k_dhhJ1A334F5UB^{e6AzvKjXM zv-X|I6CO(vVKz4wEE=Syhp@JtD=2921<6($$XX@5+ zmSylbC`N`1)OZM3jX#$|N;%B56`eRG%WpZ6AmzcMgwm^V-z7Cq&CbGSgv&lQH!Fb( ztj^2$U(Sjs|T>TU#A+r^67j%0|Ok5QS+6Ln@pm{+XUl3pv zUw&0?JV)b~ zejOQ^ST-0^UF8w9vwA>SzAo=4?D720c&(UzLv1K%Y9GkasAJ*`(m8$F4umQ;KYs(z zY~286UbI1@U@6SQ({s#g>UC7(1q9kc*3SJliqrSq>!YU@i!*{u3XoI=6K<8Ops;|) zWMKTaoOtCMBldQtcK-<@+Mvj~5$IgzI6A1@Ewk~e%4NL3wUN@Ft!4m z$|Axadtb{1k@OMpm$U;aj{P#v`Um6kqpV^M_n-a%miuw}>#t72=&!=&IuWd#5`rmd zHorLtVOVvlD*uA!4^h*k6v-Lwrk6>XJeMwYwT;VqEM{-a(Z$c@;0(ZQ2|@=9A{GZM zFmGcm{OWNkbEg`Z?htaL}Xad_+zTi2Z=$S2%r=Rb9Q2EKg@% zkP73QQ1SJhJv311;HPR#;!qJDRCT*w$YNjlE4sh4b8EA>&l2=8H=mSUEZDGuZ6FF1V0wrVakHlpU|9;6l`3v~yH^>hk`i##&X7GJcx)6$r#7?xn$^X@?M z`*Q`$jUD9PW*g|T#oeZc33YXH9jy8isDp$0yK7M^CYZs_N-EjPW5m7%j*z1cv-S1% zmJ)yTXY38wB#ADxlW2veCx1jrlZCRNEocrt0f$;!nXk+BEep}8Iw<#Je#nY(IBOZV zk(Ob-J{=(9Lw;6V1p*LX#=$04mp@+oeDG`%mxj&E%ZcxMTUdIfqxh#h?WWq1DEWgP znS-6paIWBchNHD92SOPvZhZ*lnOyla6l36I@=`@V!vX$U)^CA;~hw#&wqCE~mYZAGCif?eicx#Z=gHesTbh_pSs@D+e`OBd(b3ovhQB2e0zbdK9#>0Ogjg z!$;rmUTI(bt^t&ARK$5Cqpq2Zop7mHZ`-z>{?=Hmyk;V?sFtlGM_Xuj-&^1!JFwj> zaa|;lggs`LI#MO2jisBWGPxSA)GZ17had~3x7*s<3}w5T7w7wP4dvEjPHw!`3gost zSZ6GwAJp+6cr<04GQYQeI<}Hl*pQY0QPi}Mm>E)WHzipR27c|ZfVB0oRBj-{m`S!YnhuB-k}7U9RSE!`>D z>jDN9)afZZE2a} z(UoMBPz&hrvoBe?3ZyRCuOr+&kF(el5^byNu;EH5kl^ z13U=a9JIC3JXSvi@=jmJE$DUR|^&f&U(Zr{RsWXM;9YJ7V zg`}-dhDx-=JlB2Q04b?9^RI&+TzSG9t|8XBxa@^|ZkR^M(Nrbp_$O)(!>;f`%j{XM zsN_D@;PH5G6LZan&*{Z%ms6`qu+crTFmMBYzdV`4d81fd$dSLC@@#67F2G=>hc~pL zSPOgd`8fFRf3^$#_&R9GSX7P2-1Gr8cNWaww~y6$^kH4;;pa;gBBd60{^XKFkIewe zI7JBtwt6ob)Wxp=XBi3rR=ouksGK}#^y#umM0$p?V~dIegLj%-%kF=wPksD3-*oys z47RHDQ#L17m&&!~G7c6R5GVykSt;H$PurKL*kZEPnCG4zBe&zIoE2XqX}R*UFmtasPHaU4jR8+K>fD7C$D>N#V0dp3MQd&S! zxgibsXqzmDG~oct+gL)S=O$=CRYuF|y z!hv(^o_=*Xva<+#aK(~3Qdu*NVw@S!?IueRL5V9_pt$-b@amp&Yp=Vx5s|XqomCmM zCCYAJ;sN)@T(^d~Int$e!7#t5s1y2iQgI!Rh^B3P$cVw*r(?mft+B2MWo2c#oljy9 z?tlxF_1X``g<%g1BBgjkIQU>}47ts*+fCm$fFv0eINi@8?sS(yc5{9#V8z_tfi*dV zQBDRS>@@U>veiF4T+=$|f!9y88>?0*+a?~%%C|RiaCF@GcA0V3imOf(A?z?PAI>f* z1v+V!$@LpmGP~1OyL$w>fS8zAa1;J8Y_dodQn->N+fIKu1aJ?djpQM4>}Gy5c72WX zkDDv<7ZAeEBU8H{k6rt=jHSa9Ee}d5oGk+bqM{Q!%^LZ|#hR#klKI?BS$kJonh!BS zS4)=Hs(C3cR#Qp@Ouia6hYw38nytU@l*_P+lD%PQw$@s_EnryO!HiqYhqv;|t<^wk zh(HDJw?xfB%LeVE*5q18cg>sM<2vQUVK7$?rWq)Ocvojy`p_7aWr!iBhz2bUrS(%77hMBFBlPJQ;}98LHl2RcTePACpa0qs5+Xk9d{%Ff8n#X5c_h=)> z7wFzbwh#@|kuK{Pf=k4lTyq!{bda^{dO6Dc0C-5t6&h$iN-RXO4`e5%*Yr9krx-fL zn80B3xSw7&#eTe&5kx~HU`3W5rx%UO)4P@B{Sm0?INh$cPsg}oma$w(1(<_d=|Ov~ z8dAhsFOI4>!@))eeMgivoyOQzNAw~pVOg%bWAq zfzr8b3yG-@HGYa)V;+qfLZppUs$%SlAuf2cH`INl!G{~MHDombZ=L+4;YdA* z#upf3B!WIZxs5KO<d^$d2Hxt-KjYAb}NvJPVy- zDPg@(ZJvb!2O5o_)LdQ*ux~38nm}- z9u>rT{rNDR_cyqWP9Oa1*v&sWD;Kq__i?D}yOJ>rnu~Bl56G+hn=wv?vqbLowq2EBblRcT{{6b^Gy)TzWKS@jegT}zQ=9m z+6%$?J%JIC@3Kw(K99X>)co`}W*g$-1x)(}M6c@WvuVn?+S|8&P&_I8s3)4T$9&B! zI*7JsN32hD?~>%T?s%K#KJF#|4M<%_TRRE-sB!Ve=*Wn$4>@&nX;=WcCt8S2ziVe_ z2Zf4}ky;8TeG_X`Nd6c&`X{Cfy#s?M@W6VN1dy?CCZ{S zdh2{LHp%0;(a7ImtAjHnNG$;l`~K6?P2Z!@oGfAvo-Y|VEXX%2)$%ax-4Y~=TUe3r`){(!MGyfsYQy|vfg8*AkLJ^kpJCtwzZ}7d|9l?y#Qn@I zu=nwYZd~h4F#Z}`zTq!Ham1JRqU(cyDkFM7s zW(>b;(8{o=ng5{Mt^?&*sW2$VV%w{0l$O!ZI4ogw?BPJ(`{7{vc$*0*H{bpFVPqO| z>&pOv;N7q?iuECG<9re;L0V<>Gqm<-G;E|PXInPDv|e5cP4gc4v;@BL>>sr3vh*WN zOU>Ne8GKzs>`5V+?6Dc3n~7+55T{@zlCyFXNxMP`b#K;LWOr7VZa5e$E;{HEs?^d0 z-+7#=@^^G#P1@uzpfM z!DDInR3|>o6#ou;;`Ph42%@!i4F0*&-J@ms@_`py(e8;LiD>_)cNTW#rv$*A|5IL= z>DNHt5$9X53lOn%w~UBjQBe^9+`-jeh0nm`Vpp+W493fgQa1Zjj_1}$CC zL=E!=<_voYuK(sTttk%Mf^WsbAXz3MdoG?mq44G74a)nh;IJpvZv<(2+fY z$1i||Rz;t_?EUTS4QXyfbA&XULXj2mTNm6NUm=e_g53b&3JhcCynlZK;ogT>Auoaf z88wuio?h06hqRe}@j(KSnC`b>0laA2$EP6@L$fnd($XRT%jg*rJUM{AiYG$)JgHGe zu%OzlPn)jysg=jVTR7`1QPbra4RV%yeT~5GY9RDPouHQnu7zc&qC~~FIy)sMlGt2! z+=KS|Q{0c1!nym(4?;A|Va+n47cU1%)wsc(%9PQ?UK%Khk6 zIv@CEpS^Rn0R4LUU=El-iV8bHNoQ-;RBE)r>+ET z%R|U&am3}*GryYE&#}T@i_1_5)|6fz3+Ix*0cIB3!dbefpB!dD0N6&xQ6m9SWZhtr zqiKG|WeG=>Go9oIvE1OnPuzcQaIF3keM8=7HdMa;;_$Z!>c80wWke~8$Ool11S*I3 z`B;!N2>{kn1+=^Sut<=j;jM9ev9a*nG;bHxY@MTwE^T6jK*74@Dj#$~K`i`dsyntC zKjt^r?J*q#MujK#XX%=1Y95g?>(zlfO&O~C_~fWi;47IHdNZXeD=Q%xxinCeYgTY| z?jSwpK|rt&u!wMwT(^CTk#_l1a1?gS=cl(Ex>H=cxPEGcL2hT2J!t0!sgeU^J}DKA zzzvW4?XM}j6U?zTP{!y!(>CVT(yapyyNQ8HlG_GI7c_Gx1FN_$)Uf#ww8_W90qdrM zDg@uH<|zIpfVlCrc=aLzeB)KQg*leEmpK+h)v7)8%96fEhPS%4q@{uk<7F}Mk)!8+ zX8Y%$o5NXzt?K}KZ0}Xhhz1F4UG33isVs_hC zO%O}1^m|RuXQMcqgxuV|B2#Fz(K$lj$GAD1ZeCR3*3I;0f-NVn6v?9(; z5NSKr`JH5__CuTZ<;NP8TsCg6mJ?G-^i@Ens`-4}8Mv(XzI!=x7fhhb$A3;&Gh$tT zys*kAjKO3@b(dISG38l_n`UUF6M>GMgI$b8q6d`o_-jHIm`~feAD=>&w^usd>J}l| zK(HrB>XUrNw!G03Z)8FTQ%N96Y4Ai2k>+vbJI$IHjhH|rbXllbtTP@vbnF6}mM^in z9+r5EnjAwCoW%BMe|Z;ejc4ra02*m4&z>%K)x_$s`%t?0(#IE$lSi{H6RMgGe5oC0>M78>B@eJ3)9CM+HJn}EU@<&vd022#*y{T zdF`m5Rif8JDv^mVZE*s8V|%+oNVhwWdM&m#Nv>g0Xr{eiQW4Bo?##V$^s`k*f=wi> zcc>x;SM97NWoBkx-CFRu2Ntzvt*VvV$9>0tBD`s*o%9FzBA@nsk;gl&^}b#apLdpH z0z7+hGcz-UxxHZ$E>Y5tPVfOXFR!&%$0Q!Xyg-U#dMx0c&f6Of!(!cogM$L@Tk~?0 zCro}xS=-yUZ*RCDxCCGjj)k2-c7^aO3x?y^1*b1p6TpuDc@dtqI%Rj~SgmD%jLi5^ zdkLR4eMkG}ZZOod>_5n|qoD8o`ciXwrJ;=o3sV`1I7!0 zS9?FF zlcxEeS&NaZmysWt!0YL>XXw2#K;))`4*lf~jw-xVh4*{*|9|tpXKz#j$3qp7fZ`yrbP5;_z?$*C3?*6)hjQ zjgnWv?9#7lX=|H*cv_KFRHUM31J~I1*CB@kJ1FMlg?`5;YmmvAKr1NoC@6vbbKc*k z93EsP+_3<(%-HelZ3_#F&tU;Ru+{4I4N_2C-2cSFY-)&dGvuzSN2%$0cu0w2Mfn2Y z@rxdO-=^*DKSm`NVT*I_Q9G8I@_vx|f2DZ(t4N7&-#(CK|MksCJ1YR!Y} zguU8IcX}C%wf(fBtpa;qwZF>XkSM-fbb9LU;1D`=OpDG;T4lQ4Yh&TmX`F?%Q@Lx8L7p5XQK30G`eD2a?^`>bMobLa}{!OI5Vm- zd;)GLB_r#3TXjO-u+EE7B^YJ-oNSJa>Z_3Dv;FY&Wx8h3<;<1pbp#ik*@ft2T<>>q_1N(cL8}!#a$BO@t%X^4{qJ z<0vlA8+zBp^myy}6wXE#-$z+RbI&QRba(Wu<=^cLYUvTu%)oiLc?+(7DZHeWGiwyZ zC~(I1anN9kCYdd2OH&Xvlq-<(ai%Xc-^N;HrMR4*$G%meM4MEVrET@8TVT;3UJRdC z6gldrFg@^=e|1rDyg_|aD2n&NmU9x=DdOcoSeX6KDN7*5 z^VMx?XPRi(Z&G2}8^X0{2eLRM=yM^;l&ezUHq>&H&CM0EpKh=?p_Ao>GKh=8{B<=` zq6ht~L?^>S&xh`)W_o}I@4W}z^*NEdN~rOMYsL`tSX@PJSnhSglGP<~wr!4t_Tpha zVLW#iy=s@QIChCVJ*FAKu!d3Eo44ssv&1`makTYbpA8?~@n+63z}A&jNt`gTYkxez zx$>c;U}JpenzbHoa+B(UcKG|oh)Y-CKZRR>aaWv2>~3MZwO*q5$^)z2ZCzdx4};!F z988Avy~4qhJ_bf>!nId-;%qdGSnc`^1;|m>bVf;;U1THsg7$g6pZ9Igw&}-u@syem zKHkzqwv}4?!#v7DsJ|A3b3p4A%-B({zGbvDBt*tiC8&>Sg{fb0rDAI-dw9S z$!hK+LA{x#_W->dw(RNAv8?GL4=h-ud`uN7h+W@qsa-X6S=f1~kh!hnQ%-Lb+vVeG z*(AZ`dWzyJ!~T^k@+U2t8#Tn1+tQksWfN%mQw1AG`#854ba7oGaZf*SxkXTAdfebTY=?sNY$PopbYi77Lo^BTtbD=h>=P$Kf@F3Ei%*5yM+a zhs}B49XrV|#3{g-(__ENq#wU4V_VWXw3P|tXZ#DlEWXi@D(>uPKqZ`c*@wZH!{Y_O zBUDJo{*30Gr%u=)cZ%XmibR7GN2O%kv&Wtm^vCs*F>P;m=y)ybgT$rmy%=k(ZZg)? zUkgssZuyz;2eie2^WXx7&a&(gj&m^C5(ccc8(s~jsZ=T4Khf`>Wjp|XV%KUuRgw4F zI;ZO7MK;@BYI>!frD)J@+BO5DI%H79yirdm&q^&8Jk7(v;@f|8&h?&|a;@*}vz^$E zfPkti_IWKva5pS7GT*A}w)_XfC6CxRPtgu#WR5nyvzP7s%qX%vY-;U@ZCMN$&|7du z+<<^sNWzck+qx{bT}c?&t9%bYG;el3m&M}JljSCp6ENQ8f{4`w=FD~0oP3T9oiRq_ zGt**~qIm{lC3b36oQDn)@ql6WbtRToxLIzb_9CCr6V4Jqb&qN<$iuDn^Y35l+%R6N z9ZU;vbr$5=o`i;Wz|c&_)SQH{{T5>3TpcsiIeeHyNSKyIhXjKK8_&~a*oC$_p-yG4 zJ(%BG|4PH-jTnbPQZ}G@O|RTZYhioggkJgA@wcNhXER=xUx7_vzeWH|W-*)qUzAqH ztNMGoRjnXMf8_vsll~`nmV5Z$YyB*bgQ@5Z|u_6TI5`wWx0S@oU zda%d}OpMxsdUM>fk0$SMmjk?<_0HCpay*givj~>hn)r(M$;4EIVWFpA*MCEn{H>%z zz0-WjqGC)vLSQYtzN>KZt~3+9TxT9@YJzpWZFH0>uQf2G%=&_1MDC1wm4d&50qhmf z){=XDEo}({Xs=GJqO1k4(^mDI?>%H0@=4f;^PZqc7TZU)hu+0)w#-#}Bq4vy=RbU2 zG0_EUl;8GBio5kkj6S|3h2zu{n{Mc{*H3cs*PYrIYagDls<3RX)zVw-jw5_>F(`5N z^4S#VO&@bFqb;&sob#EJ=2HXe#r^%h5yXONqC2YXLHVy9y*yWJ4M)gi`D9g7j8$_j zrCw3xwNpp(@WPc>UmwZSAb{=P(#)`u(ef5_7!vA?xn`I!$zpG9TrJ8OU2d_YkXLMO zY#Yt3n*fG7-Bi^LHjU<~x~=qwPOl4LrqN#vgn8Qk+%4tfPa_|lK;1!92+-x3ZXlTj5eT$bR1GIx`@1-&MsJ#r_-Ky3 zmKXt4f1%0NH5phl!#wDoL3heCig%GSk4KYpmh#zGE$L@@Wd zoyoxzD1u<&z0^!+dug?B*o||t*}!#)eA$w&slVkTtFAwUn-oC)u{bxfBQ!lN&81z% zr+c6)Hgv5+01KYQRyD2zQOK{-88IQdYKc5Ty4huhI)F=%h5tRAaB5WZI4 zk+0udnrqXUYQ%lRz z>6ymauv{h&A{zrMt18Z4@jYv~iuTwR=Du6~hO>tOiYwIo4Y_CF0I6u383*9L9(YFE2Myrxpdh=BVLZB1$ffxFxpYh|pt zuw4FpJWPrOh-$Idzo$FL9HwXx{D6>f8w9N;zA%&kg#n0J!6c^L?=8MF8YYTp-}Rck z@N*` zQfnT(_dUNPv{m+;QQ|;}3Bn*LwViu*4kzX4GlbdraZV~>V}*1T^*MBeMaRfZ3BK$8 zIvy}-pcw?Tu1@2t2d^r6S^=OW19U!uWzrbsxmvywt4?_|f|R!0XxcW?89Tw#)<%p; z!n@6V{*3eL^pSi6EFJgPz%&i6`X0nXS_HT(R&pl?<^dMX9I&^}^aZ#B@$`+#x*v7{TIlva>zg{VurmEXDE?&x!na{ufY*0t~PV{{go}HYWI1aO-}aC0KAV z9J%=}LdT%zY8HXa0Q^2~6mVU`5fX>aFDHQL5O@*@Bb<9{j!d)F-*F2=yF#O$enSYit&k3zlw5$Cxg)=Ufr#ISbEahtuJ?hGEAtJX$`8lcG$E*(|O z2p9(}1}+_AFTG2)0vBArn$n88dZK~s=~9OSUO;JC8L<@KfdHOw;V2X_#AMsm0k;8y ze`S{OkOz$8*CvM^w3 z@dLR4h*o07uG46;O}waEyMW}xn>4{KWXGT6L)2O>BJ>Uv`kb-uX=wyl25K|4#azG9 z@!OrvsE+9e?1;IeaAmVF_94npy=Lp2BW33>({|JrQIm5V{*E#ubnL;_;jQ z3<&Iww>`mdrEIQzXmp?6w9m6}YSeKiB}jFK%PIGNdy5@z`n7c01wp6%)<>dFD+>#t z5Z(zekFNkdM-}+4VeO&;6cjCu``{uoV6P}NAc@ciBOW@%?rRMSJnbEjk1+rI%BtG( zUqE3I%0qQIlc1fa&aXGj2waIa50hA%_CDG5{2wv%eYT8j$XNh4WEp+tQb4(X%-@_) z4*7vtfTEe30RsJbR$}ot_sJPj3)MCO#G-ZJ@|*(xDgdz$t?i9s0X!w_XVk7;Ye%7D z`K;`&yPItZPrf}J!CW!63z^#giRKTx?Z7@|yubAC0^wqg8Kr?QWNy|_*|~vQu3Pkz z7~2{p2vsZD&bIXQ_0^?}BV0g)J_mG+$u_SWdSX8J!H5y)D=?wJH`zU9RpJF*(<#3C zb#eK_i2yz9PT-MIT-VZmF2Qr0Z1lcSl-{7!$8767nCVw^0;2dVmDZ#^aS5@x>sK|h zMiZGu<_eeMy?_-K8G^(@7n^t1Rh0The=S0~xjJ@tDJ(z`xOqcg&SoGf2S{BBZuK8I zMm74)U>Br6RnW!$TlLk|mUSS~tm$~&a;9f!r3`BnUtPUv=-Neu1P%j&n!9u+RKD-m z8InT->slXEv*3c5gPc6EYxqow#3C~fJx9o6Q;h7lp?`8`cfGy?$X2n;UQJ@5NDdFa z5(_CXkQ3oI2f3G_%S}cV3m@BJ`HH>CGe9>_%UkS{+k3-o6z1#A2O^EHlqA>eE0pBX zhHkwAMkVyX5gYq?gIa9v?S7I0GKpXSbFO+T_Ue)W3R-6^iZ%j zjHzkNYLbf2<1F9X_WoPNi(+YZ&oomh$=7h|$E_<^FBhnvjMBV-OqBMoyS< zRzR&6H2UADs3@Yh@3#hjP){{R1Me8GfRiNEHM5^`9sQ}5lf%xdR3a_1y>kSF-&CN2 zN@Y+j)Vpf6U_|P7y*#}T2tO!PXT{vFvfppciwxrVzF+p@?iqi zg?{YKC*FA)Sqr=;Wk5DiOS9*a-)ff);c2(d{h#0D$RAHX5it8qGe5Rh!lC(R-{;q} zpmv>g=XD$$&}cgtd4t6UBoZ5g!_-SG3wR)P)Rad4*|^i2sU-L5;{}f>YMCJM zOgN8e=v-f*@AfrC<`{XV7J8AO?67<-%wv$@ff6sByq9YipQ=QT}XX4=% zF3zc?x0yLI!BjTW-OoTEtE*jT{hHGTJZ#{Pkb@!<_j1l~p0!<@(_uk@_%I%Hsq#aS zOpH)+Ap9TrBp||f5l-_OfS#LbykL0okYB<7;R7%RyMogK@L~DG)W{ivgI6Fr_cQ)qI}Nyu6ciYnoSbQGIppmd>VseF6g+*(43gT1MJ_1kr6r$# zE50EmuaNczY$qEx+ePG~hEd=o=DmNy`sKUpm&PY1_VRqxDl%x-i7z)mxlcAH$69;8 zZs7jC1M|9GoGzjhcFI3|*q6K=@+30xho+^d$IVFXtgwvMpKnIJw@{xxv4HgV3n)oY zj-*4SbL0**Npt@_<7Rl|Sg6Kpx+ti}a{HO)vf8IVWwy%S6^YVu6)z5HRA)X)dGqyR z-QMnr<<-@%^?MS=RvqukM8VCF&bjmZDi6OkB+>Cd$cLI&>CC3xpJ-ht;zWW&9rnNRZ}@ zv>j`2XrH3%>27#5PQ&2W-I5I*nsN4^tL`sClD?~vTBYww*eapW^vJRi>o%Qr17G4x zsqo~eJ(2YB`YakRWt!L7btwIfB$;Bb%`uiJL?pjT)gQ>-Eok z$B&n={}`kIfIF+<4ezr!`h|xx58^`SR5Vu|QS##Bs-CasHY9BXakcRqi#3bdl*f)u zhs#}^Z5L_+O||N0(Ljcx2uKM;Zm7xyg&(3o^VX<6PGFbGl~N#wS6v zlvUYQFhQ;AW#Y?9Zt32cEsZ-gd|9EH)17`VUhWF0O|vU&6>yeKPp83pya4ijJ{+_*Tw2>coDnpCO2E#M zjc?@ZcawLUW*)VIT+btP^r*G+=L+8B;&OEd3?5ywu()nu9PT8lm>`mf>A#XA>c%fy zB-hOxo7?qOHB0Q2R#k^aqQiCp*oN}urKP@XHSBa1DJTd2x-cXRZCF0A6RP>O^A})9@qn*IQSXcOB}|lrpBhr;LVlU)HwE5F44t ze`tsqhJymL&1Bm{x*PX%dHG6u(&Ws)&%?+kN2Ucr%U~S9_?KC(*IVTn$^K4ziBGxE>Z#De)+Bk|TP-`^-^v1c_#PDj4OEGmgGkQl|3EHcER!lQShVi#1sb7f+u zNw{w3L>IOz?V5LTbBZN{4s)C@K6ZYYnqNy&H&@$WnVNTk{sCL^cCQvfe-VfiTJ!?N zx7(=Hr!X8&dd%&hfG=an1? zYu!A*ywti^iE$UO)$XeG>YROa>?)gxh%5*>l!yWdF8$)>{_fK7FIBM2>8rIlPmZPW z1F7ivNiIvX)yi0ji(8!Q&wR!m_p~HCjpnvm)o8XVMYXVuDPAjygeTO>n)>=d5SU3G zjQXF9+Ti2C>e&>&Kn3*+jG!h7zSfJS-dB1_r_TE+vcCHYwzPTb#m8n43&3*tzzSU^ zItlYzww+`UalYk3lFSMW6gS(Vh|V??(Oe-(VPDe+e0@dBq{{$T|CW2OttNjFPEZE@ zUYUShFGctctCW-pw4zEgmnIcyT%15W+aQ7dr|>ChbV`b8)Nd~bLunDkjlla@`6#FM`1M-J zExRX^ohFuuJWXKUzXvzCWf7N|7~o%?=lkbz4l6h2Wa(VZzu@URXUD^ItiONgEC>bv z#;NzWq0a_|PJ^L}#SGsDtsMEN>rg$7xjm^~Q6UCXanO!u2cB{YgsVlM3XoFe!m1|u zEf*=>3&J0NAfmD$H(+2wcS&V!G^x=xGD$!ve`cng0;$q|pq>Bz*mj?T_Igw=GGHKzn z!n8phAV2g7%}Z{h!KtFA1#l1|++C14C0eWaq(Z|!x<4H z3u&=@P1YOBJZ0WwA-E&>VuHU;A?<09S|-MfSvRB0yk`skT3#tJU^rH&sjDlC_#GhM zYzS2i{13Y5#6cHv550e)22~29LEt5yL+f}EJTVu)wYnVAWgw@Cx^cq_`We|Q4y;oQ z3p=~s6**-*cx4)CcgkR9&~QRmX|6AXL#gAK$SC4|7X@bgb`PUr#FQYc!UrCz%MU$F zqX6RHNz71HiyHn0*JVGrF6G+pRuVhMOl77rgS8t?a)0rF9rDoHnBnYG^yT`-P2>sHGwxCJ0&R`He^E*z=<`w7`OTT>8655T5O@J!&t zl^nxPG73fkehP5P$v|5Ux~}k#g8@?TULUb=7ESQ}`Mw6)XQ*{2j4@SeS2#6_Q%Iiy z$O6e<)TE)ODJrw4rXs)qoK}iotF#kTfnL=)BPCVyIgBZnU}BOCfx|&Z|9-x6wdCSWsJ*$pyH-&@72 zybkv)>JICaswp6|F^O@_)Dh z$^5)0$J4Fji2wacahXY`=N}@Cxq{~(Kh^lN0(q#wnu8hfwJIcu9rBW_d0RbdI&A(Y9Y3YIkF;;)W2b zCfFJ+V({^EF4PllU#t*4aXi>mczmIHwy6`Dd(vwA9SzHsUt+7@ZZvIv?KrKr)y=Mz z6){t+UdiycK#HVCh(U{ z_-$qR6?8mG6>=Okv+0|2Si~3&!T!bOrkt>u)R8!nu`trKvhMg7;8bVrhXms<0YM?- z-@POIt{L(~|Mv#`>b_=Kpu*j|ce^*qV^FKA4(g0Ya_CU?o)Rim&r&X!n4b2M9ZI}0 zenbTP?DG%9YHMqkjYv@O=x`hBEAK^@{&#n6JaGL7kYufnv}1RAS%#PV%2n*aUTRc za^O@{K7aMFh?pRwO-q~2pi(TT1uoH+Y8y^sSw z`kIun6FmgNUdR6oZUk3ZO3l;L2}gNbv*#fo3l@n)h3I@Ob90y1^y_apv`;$E2dvHQ z*)*f4k00lVvE6z5mJe!QM|rP4PY;Anl;%D#Pfq`!Q0y;Xie>^<{$Q^E5{SzKY(BIK zaYUIj@y>{g3XhSGec9#a<*BswT(z`8uV}g3Pbc-t?S~jYeSD{_*k%nOjiFZVQ&LU4 z`+OV-Gx|iL(YbzG{)D-KkTPpLRe@2B9-ONQv8}7D{6i^G+yIqz3Ou)-S}%n&04)MYmj31V^TD#bJXAzArLIQRf`e|2dr8pA zD!wuD<0nrHH|T{hX@!L^y*H7V%lmj<$13i=*|9xN#2 z;SrbW?OXijQj^K?(lGFf?q74@Pg{FCsP2j8`6R1poti@ryxy&D?WP5x?54ln~fi8}hs0QJ5xMR?xBR;mXeMs#9PAFr5$kscE z--1VsWIp+Re{oaNFeN27M_4JK?}YhF1GpRG`&tZH5-`qqvKwBBVL5+gI?LoPY2^Li zG@8oS^!eW~`|Xg|#AoI12oi#saOv-#Y{`=*%l|x)=lTAmU_1i)Mld6D4x|=c@m=tQ z$>|5-Y2ab-;U7U68juc5^9^Pz)`#&YT%dyG0QhrBU%?O;_{_}K#Fp1y#u_+Zk=`5) z8JVEr6d+S@h;F8j1_W%#u4_~|zeS?L$Vg44Sl+BRp$K}VBldgj4)@o31#4q>?}&P= zKQTS8E!T3zYS|hUCpe!V-WjmB;{bK$^9l>UiFXdz2_owj64qH>E77?I73H~lB{P)k zZY%8sTYM%1B;NAq!{FK!EPo+8IBZ=k%vY~MNU?Vp4JrxA99qjAb#KhzqfnGd2kIM! z$-ce5K7l{YDw&$0U^TJX5_{tQ{9yaJ;X92%Wo}IjLh@*1&+iQ2zJx5ew7tCqvYt+} z`-^1a8?r|ui$t&FEIT*%O?0VwvnbSh-y?eaZ?39gQ1PrPyJ0_m{ObyUJ?N-vKU(jq z-@`Ajwr$ZC0hIWjyz}%g$4Rlpj~Xhc(BLZEzpvctu&%bgz%W51M%gAXN?eATyUKoR z12NTmyQAmYmcCrsAtUi20kfkI#uo7U`Ny--oFpt9Kq3J4cHH+Jw1`dOz#sT_8H7gh z#TK(hVP^N0NqVe&e63_i54V?s$%47{t9b1+L&BbU2Vg#oj024lkhrLP`DO)FTC0L? z&GgQ0iW8qO18Un-mIUPuM#{DqWl)rV{F=_mI%+*!DFnqPZ@DcD50(W`ht*P|Le8FY z-P@^K@iT!os8*k;zo`Hd>E4S^A@M4q&mWK#x7Ji*%0y4R9A)qj&rFe71ldpff>8C# zA3rBT-AS)JGn&}8HpNd(tkJnQLgsN~nM_Cb259Rt!?k1M% zj(=Xyai2HH4+U@I#VzEy124M_*)O(Ou9S-&T?VvA!)9cY3P^pQXV|_pEsQX6@bP7+ zu6I=|rrsKn=e=l}6s1w0 zHeg7&gG4UEHv$!O>o(~DN0MV_w+1BUO?^G~L*tX)a(goYYxRmRD_pvThOx1F-rpXH zz4RRuSzD0E-dbczqjz|ap=08=nL5iTb~cUjgf^RifD#%?xaP}8-O?=3uN)ey2ZX_9 zb!r+}Cv-P{FjhFw_lfz58lO1J#q+fq5F-up%s)D%^lXsdg)`^Kh253C{nWT`c7*6GN%ngWch1w`og+jk@&}s~ z6r?sg&iNgPIIxZH2Eb1F(@+@KH@oZ+66&rN%<0q|ob_2W^ zqH>l7{D(F#A?^xX^r@QmHYQ09_%2Ua(*~t{{dzpcFIdihdwUMEjs=Gbm>97RHTIS6 z^MhDEs~=i8-(+TXs6xW3k{At;9?To;gQVtWcF6l`KX@QZ8}#r~6NRmpPfWXjT?|b1 z+4^(z9h{t;3arZS)Kygjef{P!#4P1c?C?)#IFAxVc$jm%$$vuUCo4FtFOm#GLlW}P zkM0& zRG5>vjFLy8v_j5$$fIFCs+oO08O}^7)+_mJyF4-Q@ zIR2b8*g-+x)X}hWXkL;|xUd4$_U5B6UzWRlJ3CkVlnyvb45BmzQ&Z=mmfBrdg6(A; z5h_%gO`+GAQL}yaeU%R#?~*Ck`%y*q@$pO3L{|)3a~jL8=3uv znlG_U4JxVIf((#+}swb^UhECEq5RF!c(qIAYa$WTSDz7hW@PL<6V6s>6IW*zY&b(oJ zEZ=tk-66y%gEC%Ot_zrK@QsL~K}a$KYIbuXt^?9F3{s4Fm6cChtDjtwgYtn;YWoN> ziZENcp}y*8#E>3O*z)&5Y_EKHem<8Uxpy6%4*2<9G!TvSo*Yy83Rp0d`pK>K=)_4^ zz%CDvb_rr>VSXMu;^1yiO7!ubhCewLtw+7qZutJ9foE5$^BzgwTBy;A;tbwj#`~tz zV|3#w5*onpBF^RG$A?R83I3-vc8HOCQ`DMxgnoF;8#K8dP(mc?`-& z1*~M&T&{sR@x;s%hjXq&`~bWQXY$;DT7iKOn%QrA$p+@NHWK(e^X=??CvqCO(fo+d zSMo}Z%V}fM_B%(|%RtlYB{+FBv;&4v)~XlhSC5P+VB~uiG1MYWKWDMQEGP*49^wwc zp8cKyL06>@6&UZlPE8HvymSc#VXFqjhgt=680`W&j%(LO+&n&n@JN2IegJx4e@krt zG@l<@uhA~p)Q61>KOQ!Qg)N~O>^-~d8u7j91?}3)=E#I|(B}dK z*20qFV#H6|S+CC%@K|8={{F1Vm0&bNUK>&XwkRB7deoL9XAkN3sMu+mfOeK&I4H6I;Lg$aUd^<=wlKQ040m zut7cx)V_jPOGBkD71<^lg9_+`f4U(k+Y`1>iS~o@lHQAT><|^?x?*bI3eGb?;3G($`X>hh_X!`&r0^a|1AX+Q zr+)p8yRJdSLU)2k&~*Kt(G?UV48)*{o(Bp&8nDNQ7@A;eIg>S?zv+FDLZ%q7mhQ!9Fj5Qu{}&5?5HfLijh7&$NIY4NxsF( z)Kb0`c7pNR`M-c6+1PPceYWp4sPd=V53No07wLk(jzUv0RBxY1B0(b2gYY*u2{_8> zwhyv^-7wu>bRL*~5M6Xse(sg>UY)R*&x5@a6H)`KVy3Sk0qOZrhLE~cvxZ()5>>_u zQcgloPY-J4h5_;)YfBZ>-!J?u_?U@nJh%fjEyqwGtB+%bE5AK#A-YH6^YJm=oUdpU z^;(VuTG|zq%~1IZ+XQe$Z2R(Mp@cfqiBf9RO#c#2ycN{_#&Qis6o2?aPD(a#ABbd2 zMxuclcO`zSj(_b3meY!hFD$QdbD~6jg|{A?!V=$H58xOhu(D}IzPpT|jh}26(*l#K(|i#Hcn=8uhd93u<3XWNF)>n7 z;E({nP(6ynG*<_kdGutZ-uw5&3>Oz`#Uz|`Yji>9N;T(*2V7GUiVR>Mh*egD`u6_(7sX3M0bUvA&+P_4wyXYhpiWx9>0!+p31xNTd>|J|OBkm5nf}2xCV?C?_*o z!dECMEmRDd{`n_&ruRw@)`S{a4hPTw^!M{4jx5&r+H1z0K!-ZFw@A(mqCbeauMlqc z%nu6mLhdVWaaKl#6FNX#LV~ACxx@d-8DtihVrFY9Rbd6ReHXET|(#hx3&a^z*#Q@^m83aclEiBab5irmA8b zgN$5(VU-%>&(*-2uB~gedH;CsFRTx_diStFv+bvsdcgZz_6tS)X&TCXeD52t7G&we zuk2+Ajp}BfGk%-*DZ~VE)zRq_6PjfY!aZ(HZ>OiHb4W-u3d}W2h4azc!EAs@d&$IP zOP1g`Sc*Y1&w_USxz`N*5wA+r!(pD|Vl;RAF8?a%BNFN8cW-g84HKcHan~}@L>@ia z7MtDBFsI^>Bpfcmtkm(h5-B81#kCcuRz0D}2yjCk@NFb8M3Hkdd&Wm~zX#FlTW|kL zfx>;E;JeT1OTup{>Eh5XVDMIRDJ;)4TY@B z+~+e5XBh$igN+Z<6lBVF`LZS&)ZJr*cs`D8{PN6mo4tchkL8nU5s3D?f4>+q$%(dp zjo10_HMDnE9ZrW~Szg|p)%YROc$;{V{R&EIZ#&-B56{nnI>YgYJQ@I~tkTi|!MV1L z3!(MSNmAw=9SQY-|4gT>1SJo^GUnx@{XrSSO1f2Dzh@#f4&DpRnJ7QZyS)<2sK($l za-Ts6X!M&&thC)j^8HZ30JBhE@9}8iRW;4NvC}+AQ2aUub_kB<-$k5_?Tyx+0s{ls zdm8B^ysP1#HD>$#pad%*KcaVL2?2TFbM&27Iq6!ryEatw0BixBpxsOFwO2hwW<^N) z{Ml2((d$ZRV0`>dUXZv2lm`MpyvVd?Vaa&V9;XB)Lv@+_R-G{+(L$sB${Ja?YYFjh zH+>mI`N0QPLlz^1FyQ~bbQw8w3FWhPK^1VDGY}d=D`vjEXCqJZsRN7{2{mHL)MM>) zsKVLayc`5}h1#U~Kq;6aB~e4M^$HZ)eQ{UoufI^o|NN-|`&pU8$lbAkdIOKKo%F$S z`_%q^KJN|UHffZigODog2gNk2M*Y_L5+vf8`Hl1`TiTOk{4-tG+;1b z51p!8m&5}!pNonRt=`>?t}^zSElB7|eqRP|YHxSOY#E?fRaYZ%PC>P;fZOA{Gx(rT z<`Kl^L$rpmXzku9%UH#BkekR@BzxFQoGr)f)}qJ0jVH!P ztq$6DPGM@Qk>`eaFsr}*J$Q>996B|%CMZ=(hq$Zq3JM-)t_4URkih#VP6P)t`CuQ|haH@jw9*2w{k|8Og?(Cd<3ykjSEv14Ur3D~*`qD6#t z9K@&qIn-jleOtzayg;E44vOok`p7Hb9c=IT5yxGK0p*$25YA};Ia0gIc^5;p>iC!k zwBC)4jp}I7!kGT}LIZrWJVTMa`#0PJOb5zlk64&9=UTLyI*hsBx)s@1;G+)y`ir3o zqjzv@V0v}h^Vu`4Hn)WYNrohn)$qtJ8LI{^w?C%vdSrMw5X^$%)<#lI7A>9t-40Y4 z3%SpoM1q!s_{N#&a?=U7LyULKWaG*&vLWC;%@0XBn}?L!$NRxVb+oPY1D+0z%K;=! z^Ei+~3gRRq=en<7UpkCnD?IeHFc;jL$+=o52teX=F}kfNFaOZbG0rmadotuLh%ud) zb|OsExIbP$2}Qn0dd4{4(Gwp8;|rvlj!!f-HHnQSC4Jy#Vx9)WVFOeXaChF>dUae^ zMNg1OY2?{@D3{U>h*gS7fJAzGJjAIG)Yq^aBZ0=yZ^D5zyKARJ#Z;J7=w$V`*hYS( z-kio`ZjU~6XQ>E5h1Wg9(oULqpX4MET6JUw(9aY#Pd6n>GZ3F`5<`ahs{&TAO+p{JW z%?^9E%kFr7t$P1!5a70U8^Sjh_VGfB$oo%@j&KJ+sn~H2ty{e*M`UU#WWNKcv`yfJ z5A2*rsd0_nipb}0@8hbG_X#Fr>X6GoTgSm{<~RTIoYbRKS5ksdb8G8WxCb~`y*Vzg zd&A*7!$AiRNQBV+TFr<7!ZU=#An?CbbBFyrs3BO)zV%G{6uo=cKJ9GyC2GG)8VX3g z*OKgKYN!8}509?S>lIsGE}l+!sGQfRo9b;OGqM0Iu}2~O_3b^AXING))Mb2E1TMed z%1v57k;7ay|KS3pp*^UK@zOJcNu5%VNu@i$fYI-$WN59GN4`pZm|4@yyY7u{egsIzc zn%lrc?dZx=oD0`vNh%VB1Xtnlb!j94wg3A6RK8ucB{q{C&#y3y3+xVB0kKLNp#!{} z;>%cbL{pjXM$QJf)r-CHF0jcj=eU?ZFBT_%u}W=87B@@K6_kiqcMQ z;dDGV=pUH6j8_?2-C2)^dgGk11;6j+dI#x*WZ~36q=#STR951lP&=HcZDOFj9h$m1 zQHq~tL+?;LoB#HH2l)+$MrJ;d@-|fm4=j~;2MFjV!`rvDB$hswctz6(nWEqf3>b+& z_dU?$w<~0_#*?E;$7}t4ecvhKHC0u;2)tSNoCwx1&g+G>zP`TPzXl^CX;`i%YB@P& zi;HuMTfA%ht_sI^DF29zz+%TJ2`-`uFJFkHygVaw{2dcp4qU}<4mB{3JIAmvw>qui zEL^M!)jS65&h3^Z?01#Nemb{7Ya!lI@WD(^SDf&Y7O*u{p%4|Cneyn-=@+b5XBr-v zK!K@SU}Nq=NwwLG?~QV;LqlQ&cVkb}SMCej#^{N0GXfmP>D^hE$_Guj%085W0{$3N zTUW6KQ*|_)TY3P;z0S}Md@5_3c7?R@I_$$+kd6-z3o|0m2ym>fA7%>$+`H82`w(QP zie|-6Qipwg{*M`$9eR|yrDZ0R4xAFTavt_|F@cKpio3gqFc!#&e6u+q4AZzYH=_o@ z^~G59<`JCF{7EIZ_I5Gissx0|e~x(18VweWi^;Mon*m};K(JQ4N~9-J3iMWDeSPQRL{^Sy<)|B>dvJcNKz`0w9pg-{ z9H{fj!fjYSfe!$H6Gn6XJTuCszcAL=_u)G@k*WlVhls4KtUFgCGikNi=hUxR-e6te@{3W)aFyrhrVT?e2Cg;0Pg?uhcxiXP)Y=Qn5#enG==A7oLsbAZynHp6Dm3~w7y2HK z7Rp1wlLtKyWg5DH5#u)3uLiUf!e*9A69+&4J7jSgY8?PG_qmyw4ME=WZx(Jd znhE!d`uz{B;*)muXJ)(|k}WR5Ira`TOGv~yo`Zo!>)kyMp53~H%r}GBm;2(;cC>KRy=&KV zpg03$aN)2!Xgt10p+R(l4dqOXipeN?XyNle$b0XoCbPF)IO?dQ2+WKkiXfIK0s@18 z2neXC7-=F+KtM%62t}zpOpZ=RNDo5+RU0*?T|tzV}_PiyRsn+8i!k=}Avcf1FdbMc1$L=-)^8ovQ^B2p~XN z>orLMQglxiKl8Z@9=MW5(le22tp<%CRC%=0nSy*e74W0fa10PJSISq z8I1fAh9e&&pwW!H4wlOIBoSeQXx6~VN>#ekTK@Sf$bO|ou`Nqq!l75ce)994dIX!V zgyN;rVD(z0p+(vx3~G0=l?(uf_)|4fQpU87_rrevZBvEqkuHa?r1B%X` zY?Y^t3LZIlTz;#ijJcJ$QV?epVb&Ciu&7lt<_H7I7@$6jyY%Fr98h16GM^wcebx!a zk@n{>GgOOg+FEC@D6}F|izp8i#pH7`W^&b|Iua?-3Z7fzDN$abfXSPJZb-reAtI4j zsRjzyeyLXRE19Ao&d#o4-=(IAz$Hi3)rH8c_qn8MrOu!+Ha3$Fo%?&B(>~Ag*#kV; zw)`}Zei_|w*$bDvBtTh- zbVm&rx_i;eeU0;mPW$pkC~@dl4{=v-w5+Y`=;;wilccKkO5jY8>*?vyCl;OtuT>qG zDx^0IjLMyxlogiCqFsjfBJ3!Tv-k)kuvw%WI|>uPMHiCG%4FEt{kzItDDOoh*$guQ z@!*_5^z%mH;>?_!*0M>jtCP_VUI+yPP*vJ4Rsc#yf&JhM0gDFwt!Z)zjxMv3X)+W9 za1=!)4v46;RVP59g0n4%*j!+_%feDdtd@>WQR~X(6e3X;x{wExwjLR3YHEJ;V(hE1 zj*5(o?C#FO*+OyxTj`11R27SZ=Mggo6+gCviVLBrh6AT|e&dZld~N3k)Hwm~A3xmP z0UCs#ga0Qe`VMjz{N~;c{387DfBZsaKTs?U{d#+!*j6u>*@tN!wXvx^a`5$$gX1}4 z?@1N;;R-&DMtcc$faO)v;86JB;(5$L^0xzhg`dKiaz}!!-51zVFU=6;c&9U4PkdB8 z<4&gmpq_qTZo)n$M#e~TpRQ&ZyT^G0v%o2fr}Oa`)kfnyOC=96PV0>S0M zEuRcEMt~KAfUNC@B%;IV^7kSxT;me7DBRt^({kn|c{NQ&2d(cJQIc{Nid|3B+azM! zI*$jMmE_e2mmh)BVf!e~owfh(v%dfR?)ZP35&es*JN#0aR~)uFV4&7M5r=b$`{Cl; z`rMPea7EVUn@f5J$SDm$ic!k^F|k0`07x@M3lta1;Q@ZyY`?tA;8Nlxou9$?ynPMX z+*6A+G!;BZF9LIa??NHJFlcOGygQ!hV?ze!BLlbfStt)lOMAu=I&u)yd3w%#c$d1n z1w#JN_Y@K0J`S}A`y2ZXO%-&E`tmDGuxIZt7D&8ZDG2Bk9*OnfEJOg|6#V)C>XE}Q zJD>XOv$ulG*avC5#Rrrsd57f&QLn!zB+VL^riaRHD;a!p&R_HDPilq&dd6Kf6u7(O zbS(|fNw>sP2VdU+hR%gmDk;<2>C5_~K>6EGX+yrL4=&~uRI7vyM0;)hli#x!b?@%Z z(A3$FwaY8CV0o$Z4zxOAyKb$2`2MicRVRFlQp+8Yvls6OW(V6J`mmN=v4{MzxHCo68$T>|3- z&g_O*_kgeYGV%r1>PtFTvDBhb+t=Do7hJe1j;aa!rQ=ctb*xXur81!S7`NY>C+V=CReDTXK7wI{8*J?lTp)z z*2Saw)|x8ntQ)t8y*5oO`4V~m$@A6pg8}{@T z0$bhqc?`d!U;eLF`*-k1{A=MK!rXzf{ujt`abSCmzzTDpFFRX4FDnGB6HhT)(em{E zXs6x-Kx#mF=&?jo=@hWeMC{8!`zA4C9rDs>VAF@wa<-k+AskEIb?oE*m3bEp3~gTi za=IAGTY86bviK5!(a;88juy#mP4%2(J7wJ2+iTFtd}Ig?@HX;$LdIMa#pCn&{l5SR zk`H3iomHNXsNrxYpI_SxJcGZ%CPRJ3=B~7!UCy>+=D=m z;ut_-kKD^Pk$mq$l17|YdfEeAdL4W!%dEy1F_YsIr7PXmFsbS>mzRcip@MfPF^hKr zgV)-i6R8^k{sG#Uw$v&wT|5G`wApMS+Whv{@}d$OEVjvsWE27ffI@0&?}16Q#kTzY zz@uu7t4q&B%V+;?j^1>nSCv$)O{qpKCv^9$HLsCOf`DES?LM2QCt9_!e~w!Nx0V?< zx!hD8fp`ygIJ$BagHaPYmAo=KTc8+yT!I;C*=4JN2U1U8hrY+GP+`KhEim9&xzmA7 z5sh2B)sJd0aYntF&xS}8>RTLqSssI#;slZ;|IlL}M zDf?Z|Xf4jt%Bs1VK^6xHv?bS#5ju&w@=^I@^X8;RLvw_N1^5#(+lz^Kw9DWNtipO; zLB3T_24c(t27f3*O#xs{;phl)Q#aA-?I>j3_Huiyv2P!Fq@tyYfB2#{8Ie$l7vGcK z@s=qA^~Dr$#!cjuryzoWTsgl<^ei&i%$%*Z_4rRRf4YzBO8-DRnVY30(rf(7Rp-GhP226qk54hz}s_^`a9zyh)@4=Cq zyt!UNXpX!<>}F+zwf4oh`zGEVUv2-%dIXK#1+ z1YX4}8fcqkF!p2{=`rLU(S~T%fL1kU*DkjQ)pzCW&d$mQf2>Hu2f{?D;3*80f8qub z8}w$2o$XzGwDSOcpvD&sWGWO{##P4ZD&n`wyLFo_e&YkG->wLmmFeavX#r3>E_g5n znV|@CM?IW2^+D(bcS;g?;Blh??iv69_(6J1z9!UmbxNARt$=P&pucO$&}rEOC;Wq< z?WS2|Bp{uETjQa_;5d1;P~PJXUNdoB4c{C?ztLc|AJunc$3nQAg(cZy@S&kn7`#QC zQF&+$piZsiw?%=f1Ln~hJaW=W_oaZpOCLROSiKo~T|Xk;cg-92wy}oEkXiZ9y+HoR zG5C!MsAb#$)wfW*pHob~%)XZD2E+McXa>M z?jkO(5DXr&1VGvIPUaL`d0-!TeV!4hdAOxT4Y-0^{Rw#&n!?18K*Zfu(MQzY1f}}5 z^&wB19fE&$7Iu%!O7jZhpQmTfMT03qyePFBWzDf;AOrwq&_w_pYn$zF1GdQ#DxVz| zt7aUnD<~FVe+*n#Lf-8 zgW>_Ok80=cKuCZu;>wHrfFL*-A%!(>m8EU~r9`Q=j?VZx*-k&zh?FyA&)hWm3Ph0C zfn(4i;_{pe@Rq)uXw7abv@Dq{9UTVXLK(%CiI#@sFfS?0@?RR;?y@fPcs9|EyD6$N z3IM}rGrXE2Z{PRWPPnW5wFt?J&KDdZ5!VWeH5u967mX`&J+*J*RpXd1orj#jZ2B3M zoBqxl5=+^tw`#Yl>sV9+P8N}sjKgB(CSf^RK_=Begn)>Iy=-%W3P6klAAb!AVU;RR z1Jg-!Sha`czC*`?_n$142HQ3eh_UDTO#>@|!rB)Q8P~GKlGD@0!N;|N?d*ta z%`K{qfB165Vt^FmZPTMN3BYIL)x7$&ZMp?;)&v6To;F8>dj^1wD7;c_Y>L;eO5okVX|t;aR&^+||X;8jeCv6k$C` z3#apgrD`0L0l331)%ruOHiq~6<=y%F%fWwp=-}YM!*ethIW|jmYnI2GI{Qj50Vz9q zsfR?P~0{ZuGH) zw{qNTp>#5q0F(k9fWmsCfgUSQ4+OrgCX17}ZT{j))~j9LxWW*+cO7Kkk+PZ~ACC8H11= z`y86%YK9GMHqx2D)OS-P(|=s_mD;_Fb*`l$20h&dKw9zx!}xPkTAHf4eA(54*oQzF zc{#JV_zCzU2VdLk<-6)=n@8aS$)9eVw$vXACF8Dw1Xv%feS*)D-(RBR<9ulsc2W=R9+cc z@B*@Rh>HXp!u#Rze{1mMfAco|-w+r2|72zjuJBs;M9wTz1*X?)|4eT|CPkuO562PjpsxZknWa%sTSMX z7@~IkQW7pee*Nvclk)aKTToHYYJwV}ijGmaIZ3oKpVPf6S|zp%2!X+Xz)ZI%)X3U+ zvFN@lVA7cY9`jRZTvY^8>L3VU*c7$}X(-4->!K|sR?g)*ux-7%y56T1A?NZKpgKRs zIP*j=jdC{RKK2Ph!$GBt_{ZRN_s*7%#yf2pR+OFwb2+oS#|bQw`iW3OrT{R7(=vux zz6y?ay%u6}BY!1~gRB=b0H>$mB0b@A?Tvfb-Be=!9)4MDaV6Qzv^3Q{lLf z6$Y(rM<}OEcUepfVC~3LOGx1Eufu^~JClWh*G?WHsuoZiO!4yQ`1A&BgfNKo3l7LL z)BaNnu;510#Ne?xW#eWh4%-__1AE>=Vb&Rb#?&oKYionl78Y~C2YzZ5PzaO zf2CfLd2T;%F3oM%Klx_D4s%5%W>f9EBCc-sZI5u`8e)JO6XsMlscyC4ZVCaJv);(9 z+u_1A4(igtSs=7C&EmeP7H#uo6Oo4YaE0+t{$Al=eM1DhC#stTd||~`8v?u8REw}RS4>)rdkTiAG;7LCZ||>%!0@Hq9Q7(sD@)t z5E}9`)~zy=ETC-%z2-_Queb2!+n18?*mb^=Hc~Mzl4wG5ccswP8|bvYP?Smbq&G_h zV)@G!2OY$oj(v@}oX*;{XX`1U(s^iViOHakWfiD@<;1=8%&AyLc$JTX{d=;hJDrBS zxVP*Z3tzd;1CYg$@7I9_(m1N(n3u=2%4BkW{v}LjqH@XFZJCOr*f@INq~c2Rtlrv5 z<#&BcW`W2^tl2N#VE=sH($=_#`BF=?xB4j1pgGRW5XV6v02mwBrdPe?gKn5qa8b|5 zFot$2&n*f$RbG}hh?#@9Ej*&?e*PI{943YB1-|aMW-;7mIl4i-o;P-8Mm=h9p~BUF zd1F*!palAH@~Js;gsv2ouvXxC9}XgTj91$?3WEOvh}i$U52$*%++fk0o{eJ-Lkvw6 z1J1(0t+@ICzM@*y7pt`DHuW&kqB%08#9G@E_B0tBhEM;DJ?D-3<^+s>)#DW_Rd@uq zm$4ktR#AdxsK>Zzq^MU~#CUSbuQsg>@OiE#Y))&~wKPYxS+vJ{$xyQnUmlpG57UyJ zilGp1wPI3USpZ|+78?W}NfK*coFNB=xsD?gz|N;nC^AR?$OkfeIQ`rpko3|B2w5XO zo^@$NmZ{JZ*SJQa#0IImxgUJ}_xZJQV9Xo?h<97ca9{A9>}(tTvV1{IzPVac)4FTj z@xvtjG1oSER^I0oz;+EngF7~yL{ynhOXRRJIVFbyM(2CIZpiu?b0Ryq31;ljBE@aC zwOTzHtfdfQ=e1G~8K5D^Jq1K{FNBM`14g=rN3C=9_V@x|M1MjzY;XE6*YF$1t+dLy z`q;ExT`7GpdPDxvwKy~YxU)&8&)=8bnyLXqG_lEOAF`I`^L~~>MIllJu+DZEJOG(1 z*?q-|8gzdEgn1y4_f_1cfk!`65w0UsB@~#DU2i@#rP+Xu!80x3qNKoDOI9 zfM6b54rEA};zN9qjVw(qaZAeGJ&DG;5QH?d=FEcqfE|5=>d^!AE|oyisA zr%z|%n5+7LH$S>rC+!ui#L+Jey&jgh`Em;5FKykNxH8t1vWdEp5iQzdxx@9_t9oSX zEx+RQkO5B?*)+?fYG$Qp%wI0m?jFvAFYcXJy>gLc>~1(iogAMnsQz%>XDivXo&#L; z&GojCV66ZzwK!_^*7js|!IxWkip-@3V1}=h-aue>yhR{X3p0gb0m*>}#9xTpOd8IX zf}2V(UH-xu8+}rqShPTPBE^vTtEbg!jPn+5Jf0gk`2M|GgjA+wIk1Oh_tGk@x3?ILDX}$L_Tk^I27Ut4 zgkCjKPQpTga`FNo#X{^Pm@;wiH&|#W%kytE<84 zy!Z0LQmAhfeZqRffTdSI^rC+8@KRk}=X*3_DD({?f@Q_U#RXgmi>l$zUxM45oR=p9 zvZt!kv!$pG;NrfjQO-ID`YgeisoXUeeCo>u2vYcr}C&)U_ zhOe|Ko&{Wn+Y>E@;VCIPz|Kk8MA^+B1oFIFFI~O)&w4%JmR|s?!7@U4FRD)x zTpHsrSp95??~j#;j`}#BgzW3pUX>;~UGuQ{M1bZ|OcL7Q1BLe3GhKtqoB@f(e1wq4`&{Y<5(< zHsVO2?(GW_;E#g5#y|NQ@6%&M%30U9|hX@EaKb47F8b@SVT}&}kJ1v66L$-6k0AJjj zR3v4I6-g>e-uUhu$6HaK9wCo4-qPj6`z~Be$rnmbQx(cQ2n=&6Kq{BkIDivoOP~4R zI2Ei-I^Po$6wov7MFQ^2^E(rK|6nLO>)9Qiuyai({PfEtEg$Axe)`NfLQ?u@CuC74 z>YnIIb4m9p>(t*65@Lgh@rhFvt2+AncM?9GQF7ypPVhV!u}Ollh}dM?SYp)%4E$Le zB_ftsK^Qc7pGhLoVWgIjl(I7L{8O=^xKs?=bmv(yuWZjl5F`Kf8cfsq*2mgG*j zu{C$^tM}um?1!jFa&T^=yyw~>KjqM+=7NOO?>(TMv4jc0u4vLdVXfYDRDdm3V?v%3 z>RJ%AS>UH%AT)i?=uR7xPirVyiMT%hfhnh#OjOaaD%VX(!NNBieVl^Jl=erC7@^Lx z`pnGCqU7)pb`eX14t$*%d>h7xsF0p6RrI*d6R`QS6TJW~fBp3;AAa?O!5zx!*Q>`X zA}A2?Q6uMj`9K!j|vcy2+2fWJorfgb1*@ zqZNnqWiuoFrmIbF?PC`+UtGMgZ?@2x{UG=>N;^qBsO#DSzTDwxlkM*%R6Z)$C(~@a|KORMi-X!@K~XW&A!0dXwA`#uAIcVQAM;PoML;s-t#;UtoGeh3@NdM~mYb7N%Jx?LuQ-T48jn{9x` zb?`OBniQr<1U-GM59!G*rR5S|f7h*#Pk6$(JKgW3t{(3^SI(y$H18=JT3(TDphjx! zcFb<^9x1|XJ=HPAPPAAz{xPZdbjwFgzkn!GqbyK9YxL1esmVwLm7DxSeyu)x4&lJNw@Zr&bDY98glU>6bnSJLcPZ!e~;GyCg{08x)6KbGPM#IaKALUZFAOc&wlE4oEem= zkCwh@$0O{~cmL2}vO`_9-Gkfn6oG0wMx3?cUtE|Y=%RzVCUbn(F6+afN5r-(A3zM} z;4069JAkvTx63-+TrnieYLMv_Td^jBZ;mSnG(Sn7K`%t@2toxd9ca>&+;OHTf3r`O@j8*a9!4FCo4Hi`_ zg^F1AwCE%zLLt+xk!Q$!_XbB~u?5ee}`b4W>|j7wGcp4=;(G ztv(LHqlf8xijPYf2}>}e`M~Nl%ysu#x`9uDw50XO?f2A}$L`J|F2^fg*)oQk(PTQgi~fa~3^fD=}Xi@F%GW8$dw`L8;5HILlVlg$tG*NeiR;{?0av z8~uwSChBs!;QDgxyz=4Ob@Na)T$U;z}6)9D@gX4=r@~rO0tE=R_ z=`*LiK$V3ciED2WgQeQ>JsgUz_hNBYW?s0Ohh5?}Be5Hd?}IK~gS>8?-i}lV(%Juq zQ!H0!sxwh&lp-e?Vz!792avDX(ouqM4T>nZphU%lK5IXpa0;hg-$G^dOTti z*L1%p;Y?yZkchcEin!>bJ|De?D4EZ?sgs}8WxjV`k5J4Ml;AgtRUDQS3(PpxOD^VG z6AI*T0ao9K&>oxg`G*nno^mNUCaJ4aWO4Z|=etHM^>I;m|NQ*NYN5@BG0AMYW=qjJ z`ou?0p<>2C=xM1mS%z~~L+jzKWJ+$Neb~W&6(P#n!EKcqe3Bt+HpZ04qy0@DguGr; z(}&fZxY`vyO_MGxnHwljBGRVAiOiVJGG$-KZ0ZM5d0BAc%#KsOOcXhOnaO(gAc#w) z_v!Q>PzVPf*=!U7^IiTvsYfa?eY4Od?*wNB?c0pD^M&AYYt$p*onXhKDAJDV-U#gA z5K|K_oO(TR(e1HLXfw8T1!q{+>L8SHaN2-$I>tSV9Zg)%x=SJkOm~-E0{Q96OF6?Q zYvZcLnQdha2Q)~yZA)V^R_V<+45RkDlzvCLGrHU>DT0M>p^~D8^rDyQ4l{994UNen z)U}@UV)%UF&b+PUM^EOQebo_~yCk@PU*nB~To^O6)2|!<221MT9GOdO;#jswZDH|I zEo(*?j@)|^ixo-9&JOQUv(X>wj+_9q!WOIx?h-xel58zQ!~$`BaZ@=iL8{-904ak+ zC1SVJ9UJMepHSW{O?J?Rgj)R6D&HIM@}&$`H@mWr)p@~3=RH6UCy)e*0(p`*V;E@h$wz^h z*TI46Teo%%(gA${4@3DK;r)M60ND4(e-&#)HMdDX#0arzt*JaGPB_qpv{uG~0+XSL z06e(K*}|^3?@rLxkyJ-aF;W&AEwcu6WGoDnNH*z8pbP^bH1l+(`@~9E^|qi&=*7E6 zMg%00hfoM<1fFyn7q#;Np0HL(C37PrSHRKb0s0L_ZU&LnzIR7O3_gf?EhQpJc6!xx z7rzF6`#U!)M$BX6d&RU`Ie~W7<^AptXb};SvTuJltZa-1W)-li=uBxKwQ26l+aHo$ z9M%QlsSQ_%GLT_4KAR(j6l|g|apo5Q1%PAkAmtwx3M*@~L~5)nb92(3WTaIl)n!yzg(}V z-UMlPiD&KY9^=AcX0##YR6W9Qze8K4L&q&v2vQ)Cl2W%lqo+vzwsPXc2?@L5Xe3Iz zDrI0`fKY6OWt&1?6LPca^r1AdR47apt621aEIpJjKqfhKZ2(>l^krt1Oanth2@7>k z$X_k_5%zq}Tv_eHso*Q-Z0et`PI-3anbyNi5|yFPazg#C`U#I#V2jFs*70}PoC}mv zxQ@J@EP$d0Jso98BSNkmSGmA|qBypr%}t%j>|*q7DE@_N*_>ZN@N#3sckg~AvAIUT zeMBSaQm7^9mC@DF34xTX2;}ke2HmJ@GHcxqy)w)qf~PDX2kpV&p}eDFl&^krwX01H z;%QK^F3MQ+r8Icb7< zYzN6mDG8NOEz4S9#ZbJqll>I{rnePFj%PCVk3(=&%Y9?<>sii;?UmH0u(}e~eTzuJ z2?mc;W%JYf^jVJe)B!n7Lh1KTs4e$FZZa}@i|D0St5cd`sj+VMF(^|&^#DdlD6+Tn zr2Yw#IKC3cdhWjQd}BlzkP=6BYHcds@>ZIPKQ@??B@mLUtLfBN{phioRTCRm|7?vDblA zwz3LZOPx~12to&)S8MfE^tgS}YlAuHzF;J?@$D=`>#CuY;rn5e9ac1O1h%&N=Lmo- z^Pj4k>d&hQoSw3ATis=@NcX{HOvyNm#Wu#RiZLe)%HqTN+=0WxA(;fG7vyK8fs4%t zgNM!lCDYE=Q#11JSe_x2AuMPp}v)ElIt1 zA$ZJTZ{P6F&Id6=q<(Awiwlmj-O^W=8J|@RU(8t-3^#A}kkvC`FwhNwxb4hCyjQMZ zfU&O4VtMSZ%{e=})))0L(R|K*f>3-F47rPcKyiOXHz+F(gwrY}(~u$nh)~~z(t%Z3 zf+A=;DB9NnHbGko4Ps=mB7vV|&EONVk?ZiJ#doI8U%H-zaLK=t!u$7M1m{g+v?sm^L@HuTXj(PQTt zlsJK;4=H+(dxBdIVm1D;cjf+tBbQz-wgaTLhDO`puU8QDvX684Gz98mkYbApSBHFn zM-srqx=z?pD~6{!?2HAVbSE*u%>q9bYUtJPr8m3a<>G+f5iWEYv=#-S?OdA@cGNMJ zou{xk6tp+ct1d`+*rT>Hp}GYjr(4q0p|OCR&O9Ig;dQRkol^&XxpCvI_n9-L@kjOI z2YM?FT#`-JNWHq*MZL+o)HefuY)%ZLFv!{W&`Lfxkp}CCaHIIH;wjeXP;sB+OK|JDT)Sy-m@V1nl=86>8+s-^Fw$2-) z8xMAN&a!s?=R9&(#UG7<`UYM5<)0y-I0YoE?aKU>nZuRzyZB;Th84jy&TS^b{ZCGy zZe)&c2}HSWa-8 zGnE71R@Ti9XO)e9Z<9W*;1P^Yuc)|*cWjX=$@uh1RYs=CvEx>kQ>zU3)N+#@;F5)3 zw!IkUp(1DGJ{L$Ut0stzhyyaK`}@EAsfqAO9jS3%Nwc$Jl8(>vLq!Zf0hB0U7oEod z3TYAAa=0Q@(r#FbepOdRWkrF1KZ>`axp}FvnU?^4F;gSHH~=LRLS8}KoRDKEo={+U ze1xwKg%UekTRd7v$EG8W=@?cmP%|%UKJUpWEhkr$-CtgFpXw2wH;hw$_>dn1F;!~G zFoJvjxx*Ly3XYHe{DTe&;mXYY9TzwbYycG}q%L0ch}%_L_2-|3)dh~A%K>PYB#C1h z0_>6iKrRuc0iutT7+4kG+q=%>7_C$5h74MR0_`w&iN&|2wQ~}(P~^G^eXN120LUgJ znC@8?a9lz(*rVX8tf($@FH|z?dfhM0P;l+nV@{yHeLk-moF_uL&co;j+xnO^xUsc|9tai zX3znb1oxCne@NT_iYcxdVMl9w-EI6E4?;KY%rY$2qepc&`2^%1a(R69Dv0k8wz zQ1WMYn&xr97zqI=kgehR-6-DZfQ6Nw9-WL#axyF`pgY|tdzi!U5;KOo+t}zA0#M1& zPnj6qy8cUoMcXsV+dVYu^n)C`u9SmPlHL4{-W&ydu3_j-)`m!qge&)UO1O^ZJ27q9@e?M)GoOuqQ-6|#B9NjhE z=I5U|ClCni8>Ij+m>j4WTV3xZZ8MOzwMFz%*l+r-Rc=85Z)X|c(7O2%NCdXq0{Im& z_|-v&z=gc5Ao859rskLHAd9(nwk3vu$ek_)%WU*_xi^3*bCd zlQ#sV$B(O6UN$!G(Sm2rbn+{}jjnw9wDeyC9|r$JWjPE zQViNkbI86a1O!`nb+wBVXho8l{a2ASS6B3mj`dt$sxcrTpkZAa{Hd@TczZ|hc2Ws# zT>yJ2KU)vy+t6W&Q>bsh{N?A@D^jwp3u#HY%H_s_t;HIgu#9U`Z+4TGT8lcS(=5!W zq-{LHysb)GD>XJ}V!~YHVd*tcHj%%N{DdRCDRbc0QE$Bz<2=y6>{hweh8qq+LjyzP znX~RQ6HyD@{LDpR0Hm8UD1H74uo?k<_dLL;B$zjE`~@w16Fv%of?6rlKn!{cBRf~< zu>R6^qS+rRvv(sZYPqScr#n5h=~yDP+dkytG6OUg8iVS&2US%GY5|aEt2??hSeP2$ z)K(ww*C^B}6z1l2?ewIll^L0tXbjj}F=#;%n|Dwe=TW)RN+m8g>dP`#9gc8uCI0b; zltIyq3S+$l*9lk~fXpp~X>8C>0jy+0=*H^oOy;NW8I6t>bDS?z#Mk7pXcvZ_*N8dw z=uA?9A+md*sEkK${ejfwUhK%Nkr}M930FNh%KeCwk_W8Z-R9$cS&C&nTT$9e@g>>`F$oj4Y~Dn(6>>Mh?4TXSb) z_7+-Nq=U}@ip5u3Q*)yF8K6O@2@|5T4;(%}SEP2kPTa~m3-W1eE)Ar!-<1`K54nvT zLvX5AR&Qpg)WSz!wKWtrm7zcQK~tzms0Vj?fV?y9 zT--n^W5I3<_wprYOZd{^o}RgZ%#mmJ?tK6h3xeBvfBov0{dSw!0MO7Yt@ONVi@m6C zGMvw9{n`MC^;3dU5aaYeRe!%dt@Orfeo%U?&y*%8 zCT3`6R?8&j=Lduo^(xkUrz)DrDwy{l=e6RLcMMZtte$=S`ZaZnibJ+f*+WsAzIz#+ zoi||d<=W5fyQ!^Rm{Yl~TU1=^L0!C#2Yf$HSze9RnnpS*DSvU);7LPmDBCY%lm7QaAmf(~)xduAb4FZ-Q1^e&f? z!l5^Tw6SEDnPVFUu5ve&?N^GTTOyr4nzs?PXi{;+;~Z_GYh(IcyZaMcTP zUERJO5-A<3FzM`##dehQ4YO+#?+6a%L+%V5T{No*x%TttZy_Z}lXISI9nPeU?q>LJAWpNFqb{hIL4KS_)L@WVc^n6H?n!AY_5i5yqXU&^0U z*yCq6`e^swu%;qq2Fknju`2ym>mH-2+2`@e3suwfHJ45OUw`}UyPYaKub?rmveKpZ zxJ7h{@etkd)JmrMoq&L4XaEGj1TU*k6=*SHgVzCSYy@%uyPBo?e9G zCseNUH;@9qSF=1#IIk`SgR{tCGoXDM1Hu*$RaErtrV=CPei;A|h45e`)@zJ^eRYRUmWh~?gr?!m-iheu72Y$c~WaZ9C_mpa0Ei{Pe^M*b~qi81yEg< zK7Ulvs>r$zSUT5{4+MqWpIBX6zEG7; zUtqnrvT~lOSni%ued%|#N;w7DCrUCun@5(J72orno!O?|0jBa4DnYff|&|}WwRu0A? zsAb+;H*Ypl*JfZOnT6(RImmI@detEm42rey{t&7F=4nHw{!J54opi9v^j0RTv%%QX zrc~`&gmoo&Tcnp zc@9Nbw05qMST{f<0OqZxj8^mc!&RhKA#eT zFwZuvriQahKnHX`Q{g;ZB}gDn2m|bF!kA_0SBmqchkH6Zv-0zwAjWEPvVP)ivpaZj z0SW=E5PG8pq2{D3wQX?p82unW5LSGWn-3Sk5Jv+P=N%e=B&$ze0TihN7aw0@WMs@h zzFE-0gS~0#>6(upj{ug{a^MNrt++a+>t7=GRP7tB*I1^pu^l)7%uViucLCS-LvvgC zqkTxS0u`^)S83O?KBse#Q)5G&KK(P>z<|Ly%=q{@yj^-qo1#f!n+>?x+WKYJ@sd{E z;f%|9^NSm!H2T6qpQh)%B7k~eKXLhCf4@l@=(b{O%Y~-jVIKoJzkD6XvG(D_WYgx7 zjt=g+Ia_)!84N+kog@o{eQogU``IZQFUPC_u>>7 zMb0)kEM71`&A~pnb?X)tpolsL%K|OeXPbb)UR58v#-y`?(MI+SQZqfmIfsq)ry=gw zjwm+B2R}?szJ*VcXp08eFY%as917LGh8Pd-Zf$NQ+Vbpmb4*Rr+ersMF~C>igys zFIlE=LY?z_JCzQ=I*EYI&`~|~UYdCq+56Orb*CK4>c)IqYDx+f0&x+t*xsQ6cTa^6bGI2sUfI(5K9K@>DiY*I4l4kMN&@Sr znzjU+b>E>YrMEjNNBkU9!14e>qVCg*Kty(BtK+|D4;*FxZO#bz9pn&EpLuyPUDtTP z=lJCT((eKK`K&5fjg5_NQtf8&)I$tnBma5C>@;Bhc=kUp1y9?jcQU>^f4G+Vqr!OS z$^Y^V%7yLE&zdGp?9+%>`Qp;9J*oThX?!NU_tm$9j=NCp`d4i;`JxPW8|@2tWYaQy zEe!STLi@e0y0#$Oy3yiSqPzA#;=LdDGLaqYB1qJD7~k}zB)6yn)531Nc6|1bqafl; z!Pr2Fo-QhSxz1`u^_=UWAQs9)LhpkPvy-pUgm|_+nb+Bv z5F$ReaA_gnolhL6GV!p*mDQ~{>=Ck|J)e4O;Y$;p5aVumat39&3(wJu)*4GbP&ZWW!^Gh#hqZ$vQ?H-HnBU#zVXE? z(Y<}C3QL{(dNONYRt&p4`wTJqny*9;&^0Ub^!+K@che7NT}pMPEM$XPa@i^HLU||t z{o!M0R-9bXH@hC8*8cRu!|vF7cLT45y&mee#r6y&lMBmAFA2WR-^;A3QkDAgxz6&9H*@YU0(g}W1QZrYkKbgo` zUIi2P-+QYZmnU#MS4lG=OT z+`3SVd;5g$+j#uusU^=IYnzVs7LkGCyz3`OQjO#Jckf8ZuH8sXu&k(pWWC*g*29?c zGTp91sXk6*Za_37$Fiqxja;;)m7ZNIL=05Xtn}d;5>V~%{DZ4U;HLbiji8sH(T(Q@ zJ3mEc=*|reWDVGWFJT>mUQ_vxiV^TjvXB&fk&$wfPJ+`sAAu2~eV%c?8(U-B@p} z7kl3Qal<;k35*?OnIGEWTFGxbqP2m)`|+Q~+{P=Zt=(TcpGo9NPteUEETtv%CdNcX z6}^b*9$c)Ri@ARPPPo$P5%t%9${pdI(S9DT^(8^{^_vn3=K&X?3w>X&yo(k+>)Yi* zu^fAD6&UEkDS6dam|#JR8e4jxPhe0Sj5CYP2?kMDt!!7*k~xT5Kzc}b70S!Yb>-vXmDNx|7 zxbBvvu<=b|oFLLO+t1xJonLkQFup5!9p_TM@cg&)pi>j~|Iptk-?zedI=%MnIji>% zhtvQ&Pk7RveV{u~%7>NX0RZgZhY?bu{_kVT@rRTnPvg7(ohkh6t*-oIS0jIr<@^8f z8_I@GmOX5}h5#|3;@Mv8B_b>0$j`>j)U8`Q8GaUktjXH<7V%eOaeIkJW|d4$i@+Xq zxD&WUVGTRqp#|FMY8PtBIZH0f{2Wgt5&-A?c02%8#5bMxhAVwO&HdYN=`Tie*c^}d zahAB}yy1{nuX(e^<|w$a@7G^_CmaqR?e%BH?Fx;lypfkjma^_EycX7(U0cV&=6C=` zBC~S?uZ%s+`TZPPl^w1Un39qz={R-s9Qx1!N5L!2K;yc|lK9LUU=NHBL2Y0rPuYjF z5EfBK2WXhj?*)K3bbB&t=)RGW4@91Sc03M$`}c62nkcp%5;c9dwMONey6wTC!{& zMK8yAgbeFnF}x;CtL4swAoE>VA!4#w)3W4~f51R|y^nb0zhu(UR9H7l*LCxJj6*91 z&bTRr;<@U)TH~Ref&2cnBdzW4TOw9mj2t`k4{}Zpkv@;Cx61Kjn?kLOJeFkOU2tf8 z_~8T*(*ycB!JCXguH_6OQM}#taZ>2Xo2H}+X%nXm?T91n*bgmFrJ012IakOV=J;=Ka@z$ui z_bOdBB}QCR_OQ?#f_gtT=kpCo>5?gk$r0YZeGaiHn}Y|xYwZ5^JVK7fj}Q|&HJr1c zLL7j@Q3l@T%Zo!iEY#3V!zjZ73rgzN)@^8n=Rd;3^V<~O+cqI93xPf^y=;@wxrcM` z6ST8kZa)WW&3kl5Vy;Q&z8F0UnUk7&mShdQFxIqpohPTefizY?CBvSjGhx79{Wm^ZK~7CY6cQY)vH@n zw&2*zm1 z*MOg@=j%r$DT`J<9trcC@l9|}{(NjlRmS_LXWN*pkH42zfYe0A@XOI(*QngwQ~BPa z=|x3q{9dKnc2%1Rh)CKD++;wVouG(_OnWj+{U_cZqH(LcN9x&dMF}8t^8;%0703oT zK`HI%({~3PEw6=rUvn#ZQp+RqAonIeu0j=T*=G~YmTs#Z&~pbXo_k_v9N#YJY3At@ zDwcBqlKA8I?@t`bHhR;mT>=dw&wnCH#;fQTSycki_@_XHt%q{X-7}@5msaT%3KUn= z{k96DnccH9n&(IIdjW#f8UmF24T>*nnAWGvxKx*GYNfn8`w}JFv9)Ff8HZ$p+(+pU zgh*&^{)j~ABW0Z`+-L#pj;=5wWu%1w#PmXEdwWvJ@D>5t5E&WHQ!tuoh@fOaN5EcW zNY9=<>*O}cg0Ha|f3f94PxtrdRqv2=^kN8tT6O6bZEvrI!IhqO|JWG;&!H0vP)C!I zY1H}`K)xXXk`<0E;V%wSTlP?bNYtKzqS~$PmG$<=Es;;ydHa-s8t^tWHHFAdglisT zXFuQ=fei56j{*MN-Pf0;lX(!r?Y}{ei^vD~4DtG;>HUgVG$BQVn<0sbwwoa#i~~Dz{Q1`>)ZRJZCVdUT7!@cV z5CXRepktSald=7gl9ph6>uL5de%6`Wr^+v&mx5GjKK@l(4kzm2okK9wz~{F_aXQ661b^vI{hP1=zPoti~43*CRe7B2K~ zjSUqu$%ak)o-Zl`99Uz{tIz!bB>KeEZ?6(>D=7tuJ-g5wDK+=1CdY`KN7nJf?LhV3 z=7=uPj`_d+wy*Kn;VF9%A|l_9mHh=m$c;_H-S~6;2Ki{^+FCAHDD}ev0UbszL-9+a z%EDK#O2WD+S42k2!gMm?(9Gy}qDOmHOt;U!5|OhwgiLWVo#zT(Qv+wtRh% zP4A~tw?`UIPT-Eg-QD%WqQ~IHhs92u$eiTj7W>&zP&sCfT>1Fctr-)2OS-O3x}B2G znbGgsJ=N9yS3QYUcU2|HF|BjD137iK(@U?kbL~-n6C+poIO?*XIaT)rG@X3{sEy_v zqv5II%ZGnEnL`XeX0tG4NET52o$Px^oNW4JP_2`rh>{c|N4ER?v6bp%cgPW(l%IC* z+&+i}#Z@o!VHSQO1|(hw*6$0cU9J^LhtX=6>d+6c1?L##V-kvTa`d2Uq@Q^)on2k@ zE4%^PzBM1s-yJv0RtpKXefL*r_tR`6!D+3A2}~pQSz235tFa096cO&_A_KJRHEy$a z3H@hKj(kY7hL5|T$2dey2qRx3Y1u~>FDTq0T#a&stH_-&yNvr&?vZOFK{j!>|=ertie- zF+9S8s+~u%bM_d*ovj<4`LnaLb=OF2yrRIW^P+d)G+x$rkH@Z>d#0*y(E3;`I{R{6 zR$;GbZtv03q4`eey9(P!^n#i29)~>|?|-Qd7R*}%n9A-veLD7x61g&67w(9e#=HFw z=H4=_%Crj`-X@9)Hb|;82nf<05}R(MRU|hdNVhVAL%S&fX{1A>8%0{Wq*1!18{Tz; z^L)qq|NHUo<8Wpi*mqplx>lU)Tx)4y=BW(!3_15p;NvU~OkVxgqp#F*3(`) zVX{P+td zSoh&E#{5mrMVs=0GsUuX9}2K)u%)@Zx4V0KvKX0|MsCgQf~d8#gRd_{Floaxx#DmJ z>Pc;GOBfnXUC{sWL|-PE*`^`n=A_nr!_XuWbdDomOlV0-35K%dj1F6S1nZ+mI8QLj zBzO`&Nzdv&C}OWuQc2<3bCFuBumG{uQ6=hufWbgPOjAnO_|L#2eKsw z4F#^)m1uiXUMcLwW39W)S(o$#6YS4&tumfCtKcmbw?cQhzV3r{V7>B`b%|77ULNL* z3ljNwkRu|mC@joWG7Q@oV2p?}y6(eeIFlAxEkPmA{O&x@DyaLQgjuB5|4~L3IE{~0 zo1B^o!znc$D{E?KAe^x-xlVN@nC-hJ?{~sS4Vt`9I-jDi2@4C8)-brp#!6FFJj-Il zs(c6z4kq}S2lqtbk^Og5*i5eHlvGq3etv#GLg4h@G513k^(NoZcak&>$*(K&>+~cG zr0JXAn3|fLWItPKF*XSIM2_XZm1LJisi=tbz3kXTw)#!87~iYVnG_`9XBH6ileLL^ z*C>`)Pd4S8c0k7L2&F(Y;lb22dEWzd^=d2J?i!vq#qh6}Nzzi`Yy(?M78;l;*1Cz^ zF#nyrB-Q?Y884fB1inJJ0R3+t#Q&4{j)I@x^jERYfs}7TTY1iz+ z1)|QZ0t$bYd%XirXpHOflJ3}w!zv}L@Ki7Ix6r2Bg9f8^nmPOmtq<>*$F%3!b9ELK;(Ff_!o;vx~Hx{}vREh+Wq+~t+J z5BEsNGYfeAS%@3*{x)+R#yUd`3fE` z?f`{x5ZgDx8~05B=JmH!t8vdP!1%G0aC37Pyv`<8*vtHQ7qfU3Q33t05?H9MiuPP7 z!69XnqA%zTlcEn=uk4#=1P%YmqNofeR9CULPlNJ7*%%xvIS93y7VVlBHyb9ck$C15 zs|)a7-@SY1H!R}S(L2DVFO#3dF)G&IgVoLJ&E9mX$WYJLF1gokdnQpLkc^0ggztON zW2JHy!&K&%g!CpsEaG*98W>Knf~*+xh@>sbP|zkyDP0<2(a{y$?bGnCC>x{Wd&6iD zm?Jkgck9LtBrqa*4mLp;E51O+wr#Ous@Lr1nbgxeP>YSRDI3mMTV2IP^bv1q%KG~H zFS(RgpP3a1pZlRd_u_HXQA!cz54Pv{u^3=EBsCW{oQ|ema!F}vX>n$PB(Qm;Ae%ad zK#y_0KL5_rq0`gSC_GT5ANE}=tAGF=W8jC*>rLJZQzZJd85b|!X{!H2DyMHRyTHyC zgPBj?-{0RA>hoVR`2Ox%g@}lV7YOh(tk=%&Lz|&pVtqf#qJqLsLS-fs&z#1!H7p0A zKF13qBg?y#$M!_`_;tBS@L>F2c4TC!Q26OF-t_E|G%VJio+(=ajeaX7%~0TWko~!k zV|uMl5-f`*ChH(7N!6mr*<4pw*C1|QeCvHoObp@8n<1aIK0o}?=lQHSNfK>~r3VqN zJm0oPrMi9E+Xw8c&xsZD5JjkYrI4SyusBr00HHaT2RNU%2-LSdy^tHzr74)3SCHU& zpT}QHc~*0N9>SFn?3dhvw6s|-=cPtRIV??kK_Z6uk)g~R7Q=Uz8cApMW!@y+!+J^i z6!p3pWHGYTsqCKidfPiNO=S-5=q)fB?5_nNeg?oDap!cN938smTTvWvzNQe9md=2; zbj>5;*sk$PPbbADxgGq5aS}NliNUr@B{Z0ypTA1UkR*+b-8~W*aoZD>$6&g|zv0-N zHUdL+7kv@*+tk)6II zElC&$G5c$#Gc$IX1u#Rv*Ii9iQL)6wmjM#1s=b=E-`BgLp-U({M6eu>T8&pN109_% z%t3g~M5^Qah=BnU2S@lP|BF8@&m}?7*3Vs@>>aSymx1%E^E}HH&RIy8J0jB6TVluW zEAmYUqDb(&Q97FD=Fzy9IH9err;jgZYH@Ley6CG{z9OQc0F}U=uSBxXvr*CDcl`Nv z5pr6WZJ-mKlCrWV3>f7Jlk@@mDoS}Jw-;}PF_=wADYWJn?ed4OPqTDPvt0Y(zS9#E zSE1kqAn>GIX`AT09|Kz=I|G>5q>U%A0TIr$vLHmb3!AMgvf?c26aFPZON0so{jX>ET zD0%x^fSMGPmGkTtQWee2Y~q?v1>p8-Le6ngPXH#9xZ2g!`D;yVI6Ompw2+W{K7q^3-Zj@|zF7SOQ>}g09QghPqm|11gpi)DgYc1xQu*@MA_w{w+#79x z9a%efWfE9O2GEbapJ-+cc>oX5W2;d7&p)r}nMb>4_slVmOigj;)(`GKynjCuzrWV5 zXfQpOWg=+7S&xq_&~7nZ9nQ2VW?n^0D@UhnuBUe@3?{V13e<#zu!$=wUIRf>v9U>d z^JeofG_~@utnccz4r=cjx2hXouib+W8#T3IYR#{i4+UfRL@v#gVldw{hh>UEFIB-X zONV}zDyL5iRBIDBTnto>XIz<($9!3QY^B449x9ZSoQtm13#^ZyG8`ctQGNWFq*T1I zm#j4B7A9-RqG-q>@MvnqffD`oD}PUwyWjaM)=iV9RX|&)*!;DUKgy#l`A!|4B33|( z3tZy!;G93!|Iuf}LaB+_+X+LbudzEbg{nOF+X*Zv>>ghUDb>zKVv0hcSOgD2k(eB- zcxf4uR6s>60aUEy44ig9B`WcAY@@I$ZD`gEpHM{9)AgCB^kSdiDVR9TpxyZ}u>Mgu z;iDf#e4P*Ve<*-0O?v8G^DDPkO^TkqE<_VEy5>~zNjImcd(t}8(8x#~XO|DT+nDjO zlGBDm4>T272t07{3>zs)t}N(2B7#@VEMTqMHe&t98)v8H{9OiyjHA9Y=SQv4HIy5w z&I$$6N@)d!DxE~=Qv25rcuUxqtW^2LzABD#&U5LSMS-IhZwqoU+aK z%72t3+Q-zi29LrNthq=4)xgEIrhD+fUR9dJe=QtERlTN(!RG{9iN=JfXO=A_Sn2YJmkEAdC5a+ zU7{_qab3!##oZm|h2Gnah}>YTK^QdRaP>u+7cj#|wC_k=3}kj_Zc4lstLX-g4E}XObHdP%B_K zIUR01T&kq1+GD!APTn-$uU>UNT${-#MJNC0x37U_0lmE7ZpRLY?_7v0S*ADHZl1v5 z_6;D>!W>eQpU?O=J19YX^NreK&`jYB{R9swiS@CRR9YCET_S5B?@mkcE4d%LUY;0(LnRv9ZIH(m7 z5g{?03&m%u3JS@#^HQO)dYJ%Qng9F~N`ja8{kw1^msL6Viv_OE1yE*_2>_1<3rhv> zdng$aft`$M^`C1V9}=2KCx=?Qm~x z#A3IzquJ6ckjQYT4k|}WL5CM&n%SFGq z=k#Tyr?t*GlLGWB!O@m1`l!6*4Dzf#D*gWQBhAKA|fD)E?a7s z0%hEMFdar(CWC)!=_=5RaJCq@xVDMIS7%e1IXKin0``XWz$if39eioXtgQcFlqgmvOg}zw{kYx{ctDp(KLE;IwzPC z*gdo1RpqMsb$%;>z*2RZ{iMFW>|T|ToJ@_(WIC{cdMMKFvfuVgnH@tVZF9l$w|B|s z`*LCpe{iWA#c=Da>X5|3)fjblBiv=2c8!N^x~>pCeCk1Ivhte>$jdl3Tf_FVz&-4O z{#pC_mL-!%Hmi*^Xe?w9&@f&c2Mdg{nAO;|a>uqEql;#Pg|a}q%$jTt-`jd0$)nV# zWA6`5w$i=4&g(c%_5iQAr()}HIw*P2Jpb3k)uK zb(C=G%z2XaY2nKDS>RVmwh0Q#1AKu^`uzDJRB`6P(D4#nJiN`o;bN;qD2P+c(yJOd z+DT0x*v!%mrg%pIHmF0Bx0CSEH%(rpPC#TXpmUKRsz?Ll>&k!Uwq&F};%eVCI<`D# z5m`Y=AcK!)r~y9C*xo+WEZ>%nw(aef8(ap7NZVN>ki>$=B_=HV6^4LsZ4re>l}P~; zeo<*}_m7k$VZz%>1TfU}5XSA|u0s>x-O-*ZmmOD9pXf?(R8LEzYfi&A-0lkazyBmA*+pkOEzOwaV-kJE0Lv+@NKZa@F3j zCA-BC%cuhrM8S}3&_U#iijtC+0h0->p9*YdjliF|yA#PUYSY!G3X9p@{T}=)<+pFe zG%H=+LL)uXiuF!D;8gAI=?ytf>wx0##CLn?rIyvyl`I>z-Y)8{W;!Riu{^4G=gurW z9bFIPE7?s|(g7Nz9DQqvh8$N4&|;i`N(;0ssqs3}#9IuNDsG27P_JMHNg{dEKVx4b z`S-+uZO;!oEwK?^#RpW|htQoe74gwU|oeep@-szYWbJCgkijVhC~D3Z8% z@l0q~SQ5g_gZL%%BOmaX$DLiHq0-O?f|pT5U_rVheokW|!o;vb&pwfUv`xpumw4s{q z(CsqLo?}oFl(eeC0x@8d^aC2Pl&(7lg@>C(1O>M9w_9RdgDFjFyl{tI*0MkkDoIIw zfsXDl%`a|lj$)5zKTg>58+5c}|2ApuAHpWmDTy)nVWHN+x(JKLuozY(9XVBu&`{ng z$j`qC^9lQKj`7#Om0qRqZ^$)tCWu872*J-_q}e%O=XFs8@@0PY#DVf5IX<sk@W1SJdx11Z%MI<53$l>XHI39m0>F zuq(4&sSSs;vl?hAm>JtW*MrG-Sh)^R#VqvPT4DAL4&#t=-ye@%@2hHsbi@nS@h`Hk zrUeiola1yPJKA#3o3t)C`mo>MmJJTcUfJlV(>x<$EcjjXp_Ja@=tDrnWuTguLd^rw zk$|!i*0zn`e(wwxhip3$5&MM&l4!>%o9LI{D8aK61H}gvL>k03Sy1eH)Ry08)5>iG zlvSqi=1J_}VsBoXKnnN|MNrSOybEQ-@xYXgGwG7+fOeZn1bQwSD=VHbaq#-5B+B{U zzRXxk*+`pmdInC&AZJ;x{}V*cpk z{(_Nu1B~!pw3{y+bW1G2tnnTc>=l9-?Vdk(&i2Ps!h2~HFW+(25n>kQ(DRhH2=#Rr ziesarb*TTv8$k4vhaBP>4>v$BRO8Ksz3%q*ZaRX9>9!*Utj$U-!EOzVv-9;G)YnF% zbAb~HdB#t|p68cx&vvCMCPJZE5*FLZUwG#cb&b3G`V1Qy#t&nw$UU1UJ9Fy;`re#7 z>S&E-$3hvTS>x8y#&F+6EZeLD|KT=?`9MCJ*KR=}N7s2P4H~BRB#8M^HM+-F)hsP{ z>lK=nxW|?SMhG0>z@Q`ikl^65dbh4FiRBK@`xN>}D+L#Y(9?<3eD-vhByk(0{?>a2Mqr|<7DEekaa270$gPOLZ}5f>#mRdgI2 zO@ev9I;Gp**W@lgoqEZ$GZDObft<1t=1&e4>qE_#T4+c}YN=D1h{(X1_?9S6@@_Z$ zb;p?}k+VPbX;PteL10b?=*6Zsy%cB_dhT2aWHNzx{|wj#()&5F&67&80d&#UF&Tiksp(oYd7B7>>C?u2tG;QL$#wEz7PYXwP}p<3eg6)!H7|>iq!TALf?Fv! zR}l&zj}m>E1@@embH?wRL$<@M-h8MSjfDs@11jZBV)da^r)pzG|M>v%S^Eb?d5*7~ z17EMNcR2gKD$yq5a|pt{ z&&K~$G*b85h@8GyL6{`9d|yoAn&Rx6)3Fz5X=yvp-eQn_e9lGq+|9?2A1hu{i0lj* zxmyq*u{mX$tp^_dUSAti|T7P|rj)*QX3O=*UYacUx5la~@;Hs&XvEjA6 zwZ0w%*gMte>S5<2bkoCx3C&r|t)%EdWl?W|ls-LoWgw8dyYFmoH@#qO?CKI_Vlvct zVA7=>(b8HH1MT>=wfH_s%g8{rRqQ}tPO+Mtn?*?D*01HNuH>IE`n(L%@iDW`;IrQ% zi`mT6PLVU;Gm7Z@J$}PxLh?fZ>7}lf@sxZ6>5ubi%9>`mPh3Jb*W9~bu#$f6H^Z27 z40=Ov{&)gr-OE?-@dK+EPYM@`Pr>+AS`mm!*4>Y`Qm0yV(se7yu>A`kGuFh!KJo9{ z*|qNT%?<`*uYby*xKV1ih@H))k~;gMl%Kb=?^;8q5SRVZzU*k!$B!ZR*0OFgs6D8e zPfeBF>*I|4J*&2N$Z+rqCcE~0U`c$$U-#TfSwtiOi^Yb1H0$~I#?j%_;V0>9s~fgM zZ^ny64F!YA-TAaK)N>STeQs?)AA}6IgHj%sZD!fXb-93?wb5nK=g$X>h7%_AH*&};T*t}@RnYICc3WG*&NxVl16 za7#-I+t#0FgX#bzbH7}l>G)2fbM;tf@QK5ci2a}l%+*UiE?1{y@JxErlzG+r$|E{I9Yq%%X&&vb1-d2)J$*WMWkgG}l;_Po z2nhf)jUZD`mN6macankVu;pP(*XuJlIZMpJlh?%kzOq`HA}0gvA^G?E4CvpEzQU!6u3O6X{gcN$pLCW3kVx#Dl5*^PZq3Rk_fl+aeC`<6~Q;U@<5=fGjlVo*<*Sk{w)7=<+=32I%XurSFt zRoB%vlhR~$g6LpU{n8+ikm5@1-GzWb_c7Ivn~|K$&IgBkWZ=Kut)GSwHr@qh{nl^aMt63}a###y%0-j)z*ibJTw$T)p`@ohTc-?avM9Qt z=|;4myY77Nr%A`Dh}WFaWJZ6+grQ)EJ4<^f7{;k2fyWD0MlFiNTrHGd70Or7+Ai&< zx@|v;a67QA09Q&SQQY?JKMU%T4Q?f6Wf{&}7X7W!x~2>LdFuLgpcyPN&CTDV4PaD*2&T_}t!4w~8qBAKgO>Ajt zIqYb33owdn6$hIRaEK)!MZalFbh!UWrK6+c&TZVbe<6WC2lGQaA$Uam!#S9a4Fjfg zL`092BqiU~`8$iXM6!p%2;bdxN0B_EwjRGb2Gp zVd`!;$Wd|Y$CA57Ml`CKPsCx4h0=8EqDq#g0X)mOoo$C2T-X z<~3qYrFaoX*VRUeNbXl*A|jiE1pS*E8>-+TzR2qbRq#n(AmS4YiAsT9)R?xI3TzT= z)7;Lq_dv=8saUVss<#q`5sbGvp_Wepij?CaO$Dovpjlv=re80ilKcFe>;B4vmdK4< z_=(N_wiQ|lJjDgE8t8ljgT@6LhKF@vvUhL127d((@C-^B%Q=>- zN|#R{?MVTo}?J@T`}hfHy~dEWcyT28jhV3yUHc@Q}d3!18j|BgT!1 zi5E~?*)Te)mz|x>-4sz6BZ`HJ{#)eaIX{0A?(7k$ul#oF<7bnXlvH^!@kv`}dEgby z-BqlttXy4RR}m5_ujy6Z&Y5@-e4pYdT3{B89a5(QU%Hd5T6zbDP{Z9{ki3-djp;g zYgF{)eAHdeqVgTSaK?G^*btke<$dVNc)|rJkq#ttR-0@DA{=~PUKI!D@S5tE7gn~5 zPO-_dT5;U3Mw&Ku9^B^KJ6%!qdvP&(p6-=JEetz<{hI34z8fbe=U(JF&fmX(Cn$ZA zGT0m9h>~4d`Bw^F7>J=gI5SJDYisWtZ`#ysX*{stE*E*rM$uh+$L)~IR?T9x78zB3}G?LX7j-{J$$YQ5YMEV&Ux4xq6%7qsS`m$Iw6?PYBNqh^PWE*AjM{t^1#zZY2H*n* ztPw9bq9%Vo5A*gGGt7%vKCX~J^$Q{1$A`ec@EF&^aDnZouc1nw6+Axpq-aXfR zPgz*})s2lX-%CeliR+VL%@5kD5{W+|S6i}p@9W!N>2?CoG(?m~I1r;0p4u!Qdhqc* z*j>n5_6tx-O-fD%L;(8ai@_Dqw|%302)06tifATl%nkGULqb9_fBtlUu$STfeW`Hf zdRjU<4a2;=@(O;$TDrNJjFS7-`mY|tg(#e{ix*Hgty)`KiFobepzD^o?OZp2ny17i z=-c5nZxV#ZK?{+R;t!LHi?t2d!B#t4E6WUQaIir^vLsep`-ioyY?NGM?c3Rq<3(-2 zfPF)yx*ZUi_GV^7NNQvga=ICOx=;qF-!H!p!uY(ld~$wXcIRc(lPtB`TB@1V$^2`0 z>e)!@OAJl`td5$HN34r$1x%A?QdcKvI!*?oEY5&fp(xRb5?ksia-t9}kb`y%=&-`| zgH;3v8eaVYC#U+(&dx*EHuIpur#4?)NpYfDs0qUJ&n5En4;yf-30fUL=0l>RWfIg~ zG^m7jK)W&PEd`4w&uDXUW}H5)WT{oTKh^oEIa0v&wcWBnlHFotx#~IosO6lr3k#l`H**vzU zpcXggI_T7o2pg&YRYaQJpga=%PoG9T-CFM&1~mY9>=Z#7fs%eE?^&4P_+$P}w(TQ}W&(zd8EjN9*iQp&G4XEa{Vtv{WVceB}yBjZ_3^jCcS{(oGObx<|?zU*66*- zxw!=AC6l;+Z|JG`-rj(}KDT*%e9aGh&CJaC^)iRCa^9AGSU;RYAGr<2(7a!17$$9r$@+8KxAXB^{*hi4oL z0u~yzoQvXcX+<0e`mKM{tlf4^f(=`CrO4g)z2f2VEgz?NpXnpd+pRhwdOEXvCYs+`gCCDq!cCzz0vB}tv>c}rw@=CQt z_Phs2lZWj1k_|Mtqc6J?V40!Am;Bn4Yf6RlkxGGSD4VH@dx7Ps2v`MiiIWxFAw-T* zGk7d<9f^DM{+xz$0lZCV)|1xa5={kg09#^@JjMMSlnxJT{Q`n5!5)CvnM0iB4E`{B z*wvmNgdrJJ&odjg<9AjNQV>F+zI>7n-^;gOjwKRs@wv?$L+@MLG9y{CIvLJzlSErL znz!f?k5uK;f@A>dH=uAJ0vd0YFO}t#)CIZ=hcbk-|CK}9ryu>G4v5yF&Y$=}UcZKA zIZM|JZ3#4>OrLD-!!!id`v*KPhIkaX`r4$tc)i_eYu))KqzBvM5B{q1AbrG2yok!k z@P$J_1(=&LqsEu?YNnxblP)H()9l6ya7`~r8F$!^W_3ZucNTP^*qG^nS;+iJU_tx? zjx*3GTrNO9gaSes)TM4vm&1akfMfQwC-aV(O_G^b;etAp#8j|G$cRg@e4lD5V|U#p z;Bn>~4Bblnr6mzTFs)IZrokvDYEb%49~i-9Qrp@W1!$xb#@+WQ9tGlnxuKymi) z+|S&C4n>QyM2Q+icVfCz?R!2ylV#KskoLscN^&_~Y80LrcmNHd)3M80xVNNW*X{iS ztd(UW`NDo}^k(g;RJzcGG7!9Yl03NhyOy?MXG#X%o;H;BSITl_R6L9R`KGqE`WfTn zi^W}i4s+>+vSlM!l`5R8pgGvI@sAI$u}NW!d-TvNM>eXr<444}UqI^Wq;#6Frv<#I zwY4?UF!Z%f+5IvAnKo((nm2Z)1yiB9!0vC~#&pdBY)9_#)rIa!AdkvUPR{JQ{fl94 z*f~F4H6zo0^g!m~#ecC$o;ZO-r1Dq@06zzijLt;r@?C|8$ubaH~WDY;@!UysX+&6D^qt#w0q@?afXRP7n*_$gWVkd-N=62o-Dt!b0o{y@M|FetS0qx^vWIA+ zTI_}^UH3%TO*+kVDoGNBJ;@+*kpDjT#v^8gk%;mb^riu0;SDnceb!@Fzk=sc=^B4N zSxUKVGa5`$iA7n9VKDe?!==NPlGz`90_EEq-Y?UGCq*5MX4K@nhsMPhO!-wa+7Wbb zyeC;!-}UI=GB`zHe?6ulJ3fElOFbU13#6l?;}+eC%fN3cvczY3pk4|a&T#MEY-z`7 zUELU{BsovSUt0C7-}?Etf8+tNz_^n_cPTn$0o5W4=7#!u<>s&hdC&73A!TK??H`Z# zByQ$p#|tcm)j=h|(-miX1VLfJ7BO~S(=bot_3K02OT7A{Q((_D3*N9l`}SGp)wDV> z=TbL_H+a1*4@04xBLk8h&Vcz?{xxzaaaeY1%Nnn95RqnS>*!1@E;=H0efYd&gj44s>IbS9 z27(}-Ku~OKpo>d6M42yGBPOP%5-21NId~G5O6TRk-)I6)UssoGcOsY*cj%^APC2WN zl~qI_8(L4{Bx1fR_!K0a_PbdcmVIYar>&({dAQU@p@@kT+N#(gML*ZBg+m9YEFE4E z)dH!|K?3Rj((M;NoT9RaejR!do;qmO8mENVm|h>Dl?r zLf7Uyq-XGMJ2EK_hDCR%>&de7Ae3wH3zCt02R-6O$5igy>izsKq2(WN^6i((C&O5d z|3(r3M&<8^iv&!<-_J`#TQFzFa|NHrW z=LR09TMY9W4f9~dPM+=P83zvv6=w|5dKrhgX2$pLkHN2{fA~57%^N8OuHwk1@SKpv zwTi=C-+{^l&X&jmNtk(@;=0!uT%YfB*O05WwPBUxgx0g(3@A zn@OnO>o8?$RN*=?ioq6DBK(_Q!E@ET9*racy(!H`{#dr~16R3@8L zRf;Sq_&2(*`rmmZjLkRcDxDAT_bX5GC7}3XduTmL56R^B=_+(wT?ZF0Ufi9IeW{Vv zOEU6zzbw3F}P3#vEFhkpwzQ zDUj@l1|9jxC~~SL%CYMeRGUZ{G=+py_M36+ZJxU%(EHCteHfQ*0s(;VaM_kBv~7cR z;L*rZ0}>VmHVOJ-z2F-C>bVC;J#BX*Np6p>!h_R1MHs%cG2JM4>Oy#>%ccr{F?OTfH+ao!7 z5?o?L^W6jY3A#U4s=2s)bk!0PLT}7(eH0PJ2f$NqX^AXdr|h+>`)sE{&ldw;NG3v4 zI~(YHm~_b66aq(y1DGY7k!~+AWVj;#sr7v|tVzTVW}o3YE2Q z3yR3#qoDHe+znK<0xYF^qIe5nnKc`;DWd>jNf@;yXSKDbD!$_8*z zKiK6DpRHWCCGM1<)}H@edkIS!+!&HI56zyWu&$b)h)sTeE`g{G=o9nMne#owAz73V z`)u}?o_N@*Xf{iWY*cQ$32679N&hplq=u(6f(hFEY0 zIwy4t9{mISa&=aIqiCKhHlwbj1$K+LP`#=Hsv)S#{URw}u{;j`Vf@xeR0cG_l)^TJ zW~2f^duHsbAZ4=Ni8gPz2Cr3Z9#&{q<4zEMn6osz*VNoBu{Pb-4bbyFw6*-qOz+8-Qju|RQ{3|yHPd(YMX9j4ERkYhnrHB~`F&y8EXYJWA@)j61YtD&X_ z1cR8(IN1~DNCEmm$!UQHoIQPBLwS*5n~Sr)@iMzcS0$QP23Khoh$;MVm^ z@M|~M)`~G#a>;??r(|XZWxmTJXL`!EsxQ|^wiwzjkEp|u?Wy%8fQFt4ziZF1M(fhK zYVB4&e2@@$5cA1s!Bvm3LjJVR(?2&9G5oEmY@5(8QmD+_dS)IaaQuXu!*dXvL^;pUc4Hzq z+CM;j=7HvVFifvkM!>dzuh*{aJITp~NFG1`-J=9ZVTt_sO?OUJssrNpjYb+h1n_d& zazCR|;bf4QcuOi#cOKZ&xLq@S5}3^ePSxD!5qy>R6Ym~hLFlkHNA9%Hy+ZwZH%KvF zfNBRB;2XE^@7$mxU;SBZ<=Iobq6198k1)y3z~-;&t${rkx_9%ooj6R8P^|H;O59x3 zGM??6gp z1+s*sm#{w!jUX5Fum0I|9-;yT!v?Nw$7tO zieJ=H;Y?|>U3$H7!j`Er5m?jqxa5xaC*NEpp#X;zg_e=&%s06NSPKQ<;Hs`$!z2@2Vi0UC-RjB;{O{wt3B^1+dlohE>e{vUMC@}q{$%{BNJKB@JMzhewi*CvD@UhN zOlK0exWvi%ZBT|S3<^A0@1nMy=a7M{!0uAnow?z?XWZN?>h{8htP#+B6iQmk#GE%t zSpRMT+y>%_2Jh$3e?#xqM+L8@Px+G#>j759#&ew{6(9oJS?gG8*~l#DZx@81Lnn{t zQL}={M+LPl^uLlEF2nDt^;HCN=$R&O5!0D=8k=7tNw6LM9e+Kd)wAFkPTWw#_#jXo z^0v^l5-*Z>%RKaf#?Z`@1`2?Lh&VEH5F`x%ZbH%%B>VXM-zDd=y02Z~9N3#ITeeH?hbnmrX3}2Q>S6w<_!+rs0&pr1&Bne&8>ewE_T`^ ziLZirsd}y(WRquDXGRWAG3u0x!)pGwz9erzRato2&KIWJFNb%R+EM_+N!XzIJQ3-| z9q6R%Z@>`+k@D@t#FIDXI;;Hfln@!P9-yzOk!J4mL~1#bWVI}!JMjA;9jRkHIz~AzXJId(wDnmNnE1=Gv-A=Qe7YFWX#eXkgjbQvFW{3OaY%dxdpacND zTy=yU8fFxi5INNuFDiYo+RTxtfAaJbL>M9HlKj1{4zuuc%XX%n1#+##>B=eT$ko{~ zYU%_`E^&zxM`|x$o=Y76`z1m+yw#`VK3-7I(WS86vG%*o>=w9sirWxCX>hFvK0izO z-_yK>r?E!h6*xW!P^=g0#si+O8}c-LfP4K7EEUFBz~7t*>WNG+=@RIJo~sF*qa>gS zB4Hiheg_D-=lj2Tx2|17$Dc?7?gQ9VA-wN^a<<2D<{P%$24(Pm^%S0J=7%EF?qX5M zt^F_j4M1XKji%A=L~K!peXL`tgEt=qU><1ANh9KRLq4Cvjv#CRlJkfGI8n(d@w zcRzf5e5NF6lOP$LRN;(|@HhSwb#4)mM2+?NoI&=d)m%t=;gMnagJXP?_B;6acR@cb zjvj^oZzcYLN$__sAv~%6yl~^6pDUgASG@fKJntvSMVk&3F{^;j_ut_YDugGlmVJ^% z2c0$VfXiEyEKiYqR!ygVB zWMsD!DdOK{D5uc>MHOg+ou{v=fR5V`P8IJ6MVO4HG$0bc#(^qB+yfU`pikU_TH|KByAfR>7DxCo$LrE7gd zON(l(yTHGzr*?EFn+#wcn4AR!fnK z?wM@v_}{{^BEMgU?C#4kjOj>ucCsun1LPhPDNbo9Rl1mRLtpqj2CE)lSAZi9@WK%qPy=;K{S zz3r_hXYQS7dUt*p*V132`0>V?#kx zXfr+EekQQljx%Q`9+`*M^Rs6?M5x%=JK3eb|HlPT1$0j~L+v0rP{1jA_0(H&*p5j+ z7C%1}7e2m|$D7x%dGXI6(a@cm1|5)Nr&?XLZ0FS;@mwE!h^&Vyz0j{;)BXXckAFXi zO6Kd?I0Z?_?KQktUZ0Mw-{?2Gp;BV40jrHiKu8$($mlQhCG_=W^XcCiIdh?Q)=II1 zH*jcSp!UZR_o;~CM)7IdcL0dP7C3ow*q_5+>2j4ZqnH-q?O7s_fx+`s=A@rf8 z(1#<+@%huIS>Fe5T%^@hQo_n!yB2?2uxdL3OMCYV;x~KZRDC+m#|T2N2uPWqUX^(o zR|V(!KOpO_^cS{skX+-2u4H{fYXW!*>)&g2+M7b#h)5HKd;$V`#=cqtlCQfqC942# zHi+*;T3j4zalp3ye|!9m=qd^~2}*#kkZ{Rip-a}|TMSd?aDv*-oj*Ub(0}1S-SgsK zBm%ly#-v>gWzo7};NJoR=JezlAHQlLccgu{sVvK1kD{6pbPugUYmC1_m0M0jU!T^i zsd6e%ldz^mvb#hy(`|WIPa=kfBig&G7a@%v)e<(oby`f=VVfK9jxTTCoQ6ihInBev z&uu8j3c<<4*8Wa@HT~aS97>#e|L-FsrkNW2svqlL58*^W?GUlI9+a5j07Dz>bXLF9 z=(o4cJ*kTLkpGzrtB>J+CA?fEsONh-t3gbunvh05rdL_MA_3&jq&KqC|h1YBLR|7-nx%iSTvfx8hfpe=dD)y z5+s7nmdeuH(85zbUB~6PnvZb!h}f6f*JqiVdwObTBt4#;KE03ldL1msH$YkZd;0zj zI8BUb%__G&+da1(gDA?`nU2qcF_&n4z(M$Q>P%f4WLTiTRzCRlu!dehSW$!k;J>{Y zx_<64wTP&^B(^tuIP#gZ!?s2=*R_~mAC3TV;Bnr31Gy$k$LM$O8Xov3y#v9Bc!b2+ z*jnC~ejh$O0f|+8{v0dX8XmnTDJj_vKP9o&xv32*5}1X{<&WO1o(fVaFV11oDK-1= z%xkd{lin4?y|^O{P|P~_TQ4$o$T(7=2MZ6L2~LfDO`HRlYY&srla8!aPL=Mb;$jE1 zQUvR3gbWhNkrn)Fzv`e+>jq70$IhCG?>lJf_wz6Ya{$R0@c4qC<%`t^WNpCY`c#LN z=Xh1iHely_GBxz*9xV8|YSnlZK8W_*c|y5IBCtds8?IFeiq{9m0$y07 znx>@Ty4HfDQ5gtCO3Xn1IjEHq*j;NI*UX!;2fj|%T+gf31%dZfqJFWTWi*t46^8IhY5#duL>8F5?lL zo~_l@RfVHNot}dE10L7ikC21P8Fe4k7hIZ-J{P)mcqm`z7_G z!*QE+;}35G`!6w~e;4rTC=p*5vzxSxjj#S~v&pYfUN?MBp?0J^N0{f(ZXofzR z%>ngE%ev}gx9~IeSH-Nd3OIGFT3hccy=vEI^`KtzV6H#U6OgQrMqBUp7hy+;*k-PP zNBiUk_2ontRJLMyF*EoR$h&!*etqrg(fdQKT__i9Pgkun=}8}jJS0E3P*Bo%h*-VJ z+1czfI03ZO)M}7`8pxXt>dhLY$H%vK-I?SCfefXDut5jOzARU8>H&uXDic3a%S!F} zA2tSi50Wo!&G%_64r~+|AuK{(JKBI2l^z+z;(M=a@=Twxv@fZQy6R!_=+wb0W z^c81W5o9?D)8qqGJ}GP@XAXv$oHEUPFnRUQOLf=|PfVk-_xT?YmtZSODQExu3U=JQ6GE$~4e_j+@r>2M z{+eOdlUzkezG0Kx57AoTzhaz;0B_SlNMpcCc3y(E--rUBSEha_vDNYU8Do?8eY3u7 z=#1H4w2r(V#NxE%9x|UVoG4XVTEbH+ogqgE2!K%6-yF4TBM^VrU1B8X}oIHBEc z5MK8{nE(fYeT^6UqmKwP=6P)=kIl>nP~7c}!AFu^t-XveSc>f zD4^g;A>1`@?=r~IroMYe3*HCNi9-Rt7!X|(^$tK|p(zCw8bXb}=w(>v&73X?+@Xcn zz{~iOj?!|1{MMabSe>A~DcZ%bqS+g&!}?K<6M) zFcK?55;teEnNw0-nh4EZim$F7&R4=bVHtui)C+_XNh~xXhex^iU$8WE_1t%M@BwT= z8zO|0C%O4Jd_dEXfkZ65jt+qc7F-Vso#+f*tZP%;`unT1a?w2cvDAq+vsij{CdpF6 zx#S-|%{U;eu{#8k9k4tYr2DgaL(E3Zr$D*S%yc~LNwxBZj(^Ye_BjvMitVI6j*a0$ zTHOZ;Wn7D4T$B9bY zfv`}U;{JUltV)L3t-Zp%C5*)p&jIINML8eh!N|o5qyQs+L75)ZsOjEKF)R`XQ9*Zv zNmVci5RX%%cQ*&wHw&n33bu>YfbR;tx@^J23n1N5j}~}s=lsCS5y!IB5+2M1{rUWEC4A%OlESM4D9n^$JRTcTX(DAc+xCCCi z+Q$X_3M_D2sP+oIu3(zrl)$-3*Dm?!iWI3MYfs9(S`^8amNnHfkI;H{tB#yO?XY)& zRm*=aHW2cdLSi$x@r6=_QxDYCUC9Sx6x13j;R`qfN`=r`=zZjg28Jb|T%Q53)61#k z5k(9>Kp~*d%X>3-bf63{$P3?H3EQ2iK{`9vNrxJM)F= zsS29#bxACM-S0-6Fkww$S2}?;rfxj5lF@fju`PylVj`qhXV}^D2Jp-!u#})o+08Y1 zk)1OdEEHrj`$@U6(Eob%R0H+xgp=Up+$oB?*;a@9RI_vM-r5|eIEM6N-RN|j(C-DJ zJ45IwpbrE}|1T3s!X7ujPbPK}40o=}HG*+Ni5vZv! zKjBFz-Hp_iD0|FE`Er3_%W}F_Q6v(A306K~A@?UgDsKvB{safEzfY&(M?n`2aP3Q$ z3@I?uZvyKCoY6Pu(`SL;MSw4uc@;cj4G!HC`X#=D;_?a)M}SAafRo_zB_Nm$@7vqQ5!7XNPJ5y& z+5g!tR>oIk6pPa#!|e1eu0h3-lK;}{V-1e8X&XmN2`fxAK-8-te4 z(A=08Qk;3`ugsa)ki=E5ZEivpjgkNVbvK_|&Bje3@5SqdPC>(7IRs`E_2P>MzB>yK zo&v3dvokdC?ENpq;H;IZMy>t;RuP-iy2=%@A>Y+gFYhulr8+ zd9&ZW*Is+A-&*UpsNyRy=J;fskIs5!aXKX78Nlw73k^kJhV(|$x(Mzcq5w&0XjiUv zhMwQIUVWuc0cJfD{PDq`KKkdp)_mqJb9T7Yync=i^0*2Z3JBFQYWG~f`fUBTyGwl< zH<6F$B>Dza=|YL&RM|(O^N{WS?j3@I&}+!$VgYrJihtYNev8i@j{66F_05*?Q`y@) zuScx9YH@s0_t#gq6YsB|m?%z(G5W=yKQuJt8>EKu0)(^t%v;)D{?mh_n?9!vDZaUz zE+-4V@#;OqnbOx^e0Fj5`fsfZdwi43)Y|ectWK-+*_BZ8P0E@lHGl(s7(fVQ#Jt%H z1ypj7f|P{y>#qtAvDDYku)e1w^YSHBB1Rbf(?FKa*=lsDUV>TDR=@s2PfI((wnLsD zoxuS7ihmw#=EgEBJvXx6|;PyosC zNlB`cnw;YNV_M49#dD+|1@hVI0YYU11RJ*z${-Vb-7#~(>Mz-F`M zHmaOrZ{3GJUepBRsDp0Zj>$u%unTJ5RznB#>|P-z4~m<^GIIRD^TYor$bD zqkZ%5`zyZl>G}+5{C$33ZYhTbkq*^M{dvRsH6nNVAXM))K%d7Q_~ns2ouK!V4WIq) z-LtU54fM3(z3BjEQPI9k>+;gVLSYU}X|gk)M4bRR6lDLoh(!eI`$L?CIv(m5o|c(u z0FRV&}OU)1}~LjR#f*G*6i7azYSVb9bb4f!ncglm!r zGa5I4V?oG}9~v*TWc8KJBUv(-dTteqZKP|D1v&$djj=Ex#`az9eS(Q+Nxw6FtLY}5 z%L>=3YpNf%`?gcWWAT%&pmE;uioI=UZT@~VK(*g+lC{e`W(@*7AT!D79 z6#~4Lod4zZ z%FmcE#kqQdgThx>2RiufUqvYZsy zek_31oj(`|&Doo@Bd6Lls|NsGhbDNlqqALVmeBY5c{DW1{PBXUT+cX3J9>w2ReXGW zi24Y~>a-Z7V$t(JM7IX>F_w`Gn<^#?r+U;7)IM5Z&t!mzL%A29Au3WY9y( zY(ZA(^{OV2+V0aoGX8jcbXY_TvS!6 z{k%0T29jKGs?b8>FwJf9%^d=5X(GwlK+xl9ht=+D@p6qgQR43eZBeU-wl0~i+q~C^ zu#2+NXEfV><%@UF54GDp75M*EAuHCtGW3{#wWv;u1&DbByRd z!wIAVACzzz>$GjQ$Eo>=%b|eJrOYvvKGX_IjZ#$}D^v8~&QrB^Bu@N-kM1U|`*!yp zhImL`Sd>TBLuv}hbZS7=YTB1gi<(k#8U$EOE8%6!2xG(XA0Gxbrt7ebzGE!)b)jvA zWnNVX;n7U1ZymdLu7R@L6`&z=s_tqPe4J z4(f+BIt;`IkjJ7FbUb{41S+LMutN4=rN+Ssc>>9bs=<7pr5*6)^jT&U&0dEP>ki1( zJqS>AA_%bqQpC_ZoMV;}W`88D+$>>waJ;Cix{Q}9X#t+3WUOeR9`8bmN%Ai@&a%yE zx+otR5=*k!^Y$jv^61kuBO%=KvD^k=q}UZUqXqbzSFOYdHboq?MBERDgqDV_QLLi> zf_>!F6^6&`sExtO!u)+pMosl)-Mp6GqTm3_UGKGH=tt7hVZUgy-IcnvN~NK~qm9|r z_a0yZxTKqjj)CP=;Kp=S*yVO&^@hG79iDWclg|VmqG|ULc1Z1N7S!eJ1WvPnLf@X& zbqc$7L`N$^`?7UD0G=*&cyzm#omFdnyK&IbwwmwDAAde!ENm7FTt8cPW zJr^c7J9m(@=U2pn;B)b^H8W?+94*Qmo#WkXXfDXYOCB|sZU4UH6f!8i6EHGGuAy*6 zrgd1gqgI|Ky;0Kf`t{V{(nN(|(OYhHyW0Q0QunTP!ys5Uwq=gh!ytMKqQMxvKS%aW zUf^0axZCacHJ=|<1^jBF8G54pChACC^&a{of4K$8SFep7UxvEg0$+8OP~?dTi-&fP zK|!DX(=fu^j(V7vXX)u#_UDlcMkNLZRCCM^7No}-;0HVad}L{mPWG>t+;ot}8H&uP z7^?C=Wm#<>W}h5zUeN=4K73s7uaDoWRsJvES+YiIGFN;IlyZ0eG$CPD{@r(%eUPIM zm;O_mfgv+tG4R7^5Bsk*JQcdGTp`>7D%%|7CmMf?ZZWUSkl{3AJ+9G%(e7& zaU=R=Z>iK%xxGpH3{|0P)4eT`q&?au^ARR&Pq_5rqQla~`k-7j|NPu1zke_yzDF1S zkR$~Z{>`eY|8)oHw|)ly-X}NvPV?47+#2}JlXU6q|8)|=y&gov9YH!j49Ra?&i1cc zP?asH@D|4V21dp#EA~DH^MMxtgS&~_0>7uCr`sp@r=tQQCVhSQP8++Dsrt~&hFJ7F z-y`UWa*@TTD*g9QRP+ObLDx!OXnN3#`V3|Uz4pC@k)qe_Gg4z1%;7!KfkIA0vUDn- z)c?cq{^M@bb(AlIq1pws-q&f(uoPgqLFeMzdU_Hd+Z>;Ed>a?CJyJF&=^EdqR!0uUxp?FbIZ%(R^P+!{B!ZR@-{*f(xj*Xgh^n-)IqP3aN4O9!U5@rNVa$TE-fy9$GgN=o_{9((vQ9fn!CeT@z-7hW5J|9GsWcl@o@C>mZAs z?6ko!Temh!j`X4neM^u$`j*>P76y7y`DRI5r(%o+&d@3qc#qoqJsP34iwgr3;`h2dcTI&vH+4x-}Zg_RL%kB72KPU8d(F^JqWP1E+WIB{DHZQcL zSf<@0n}dVwZDwL(pnA@~WhFrQB>&ih0$GXY&wKaUelSemXn*>-_Itxec9+JxGkRSg zDUcIKvv40By%(lW7qiX3UFuM0Fa3gCzz6#$Y~TmH{!@kT-|na}MC{+!tpm3Qj$( zL^iCd$pjeFbIhIf$+rM?@s2660Um-I$-$i!1>lUAExc>Y!l5MVqk|v+@$H;g$l|t| z*2Ei5l$JG4fi2A)P6}wMBozvgdq#76@sA8*n|W8j+}@39O7Ss~Gru8QSAQVX4{WmK ze+ZVrpx?F+<9l&#ZqAXiY#GRn*~njW!qxe3%v zTp%NA0&FS{tU;%0F;^JK+$xFBi{Q2f1ywFQeQje$)}HUz$7Q6<-MOklIm%sla{fX& zKm3#Kc*nCZN{F@V@usxOrGC$$R|h!BKu|OcRF=#RJC_@02RCHT{egt7|0n8$WBMGi zO5DO3;N$fK%SwX<#A}^O=eiwQUp*ReaRE4?0FWUy!d!ny#0KMqXoJD4As%z%_COJp zQ3&1Af(lJwYKW_i*^qB22j0oi#E(-c;ajs^i=J$Fl)>7$!Opbssot@&2Jv z9g77Jp|Ik4RJ5MFik>Hc3JSO|IueH~n>3r=JU#FkL!P=ma;}hKN;69iJxE`eJPxYK zNx5b=N7446W9ZuuB6F)t6WF?FEqrpzbfcrA%LcDGOfL&4IArKzkxiVL5&5Hf@|h#< zDz~G%O1bg)b7ga%$VXt?GB|OK+BJF=6Xhmg(t7*XEZdd6zboM0y(M2qLcQ+_`hZy*?S}f50?5(2cwEm*f{RK7q&b9UFa!!z2MkgSy7jZY9zXTVmIB z68su1l^QI>5Z7ivvirV_zyjFmV;p+<`VkL=~|--5PEdiwAV`+`04&Y_XLY_>+; za!W`xEz?T75u9^jXGyhc0I89^i@fOMASNTrh^?)$#rsv=sUKlIcI)Y#BTay3I0VkB zopQ`KJ9mM*#2)%~^XAP6VThc(5TK_0T(*RT3FX5c)-sLXVL`k4ZIKn7_owk6R}Uyv z-%zm$csz#F`7z|;=>;(Dv$8KOjoN890`Q!SO)wHIR7#fXrCte}6L}nn%*$RBDu<6( zOuv<*>=QXtoM zGv=$j96xjJSk1axIoxi2aP#cr>SNVa=tS0X5S@5!@91Wz(Mt)Za63(w1e}uw)q~dz z-QPV|4Fq)19n?d~_I(H#(7oRBM`|t(8*Ht2;^K ziuo|!5QB^dJ#2QDIzgE&vbW*MX;2U>P znHnYDVVOH!>pU86ssxW!Vz6L|An3Fq(M(zWd?) zfIGdPMz_CA$EKt>gg8kb>-&A}50ECtG6fU~JA!BEGBUkiinA^Z#@L?Si_zo#zR`jJ zqGX#LswVF^`4$k?$J34`CMKJ9+?s!wH(Nrfu%r!5R8Sar328rfVU?VCn^b@FgbS8q z=^tMJw>Kkj08p%0`@)q{8%}+lgZA=5J)MW-U5Qg_@&%0KNfy9oy52yV-RPA;{sF%- z#Vs329}}`UD|>+~-Q3mHYN8?_4B-M;uV3HW@Q=xa?gyjfGUYc?P?A?cW()M z|LQb>R=XkGG`D*39}y#_j3$JUg706pG{Fg&oj;4P62*W@HKbqdJyUM!SH7JDpmwO! zK|;*RL*~CP1x~qhqnRo%LAdB}d{-pkDgJbho1a(0^^lsStLS{1rY(hZb@=t%jgMCO z?dxEbvs8Zn?`r~@;_L>l18L3=NU;KN=QdDjz+9{G^)=(gQnS>EzO3?2f_h47FE$VR}0oYi->zF7gbh4JKhqK<2w6OiuaHq=aM z&j&2K7;^w#wMrN7;fROnyIkw~(Ijhd*!>}r6`M~`E;ZFo+%Dos$l^O`#|Iy8v=oo% zEoIF~=BEQMP3*>oZ{GEw_g*DXEOre~)vK5T&WS<^NeBxA8(%!-dFX9HAzjcub$sVcC z@|h_tbA#d4upaXim*@;eR)lvK%4!R;ZpFF?}^D3x{gU#kxfz!beH(A-r{t%?BiK6;<;n z*v+r+g=h_(JaB0p`2`VB4ju3I_dg8?(z=uk0_%VJu(B$}F)m+!4EKOGpmG1<{?WfR zQPIf%#py#nfvwSGlxM-I&VP8vJoU)7?@8ES`w;iZ(JS#rkJW0*#W%cTj@bawU3*8| zL);;oqMXqt|0UaSRD$|Od(G#B4MqhwZCG)!H?we`X}eB-dL@{eTle_pF5e(pkZt(Q zhh6~zEKg&dt&$l_0qgpA;Eq_>tXF!+L$lcat<3k#YZdCuxn=e}46garv9xuYZTQ<=B|7KPGpMNNy2XmO89j4q2OgCAF zf9Czve@QKxZ2AG(!knsj-2BNO_43$|mAlF3Ym3VQqBSSmX|o0-Yx1`snaJD4Q2A|o zqHW7h-c|*+FNI{zePjJPSRUudM@I|>Q|vQGmHy@L42_gt@|Js4WnVgZ$BYqa2Zt~n z>}{kK0qQq47{0Z!4Z5Q6h0s@a7qnWJD*)%j6U?yUg;|vU1+6<$D|t9i4>%C>m{+B< zF=O=6L4CAeAwr3@a~Ab$)$<^R*_Pg)2*(6oxGZpK;}e0vv|jv99^d@vbcvr0b%ALa zc^#r_9$;)WCUGL#eRfIFCk!qI^V&SO^c}nG?Ip%m!oc!`PT>1H0WFhnRfXNIF+-zS zc`v~avUSfz_7hXRM%%T1qQ(Y*}6jzPrdmo-}433qrydK3O|#_pVmH`V3)% zf!i8dt>iVN~wz)?VtT6-z_FqypULe6t!H2BNBYTpyPuGh`zX3V5mET(HtuD7X$3 zD@5+BiuetF`1wYT1BtIq5eXZD@l%&s>I8^8ru2Aa)G&{kRH@P(r(Zw|pS^_E)?MF0 zu!p`YbW$4S5Re7z?Oj#V4T=z!X!%6!h3sO?Bh3d{a~Spokx=C@O$l5A=UE@LxDn&7 zp2)>60I6S8tjaczs^7 zuo@W;2Jny{;zn|jlg)h;Nfm(xVHZQdC5#EJ5E|w#3n2a^ugAF z`Dg)_z%Y(Z*gf)ox2~?;Fd8X_8vXGfUESd47rHUuVVMt(83vPbb8<8xNNFu%p@vNc zky9@irZlk-Ptir_fq|fs!i4FofncK^gyS3hO8VQmCk3JPOi`VFt~8Y4ve;UpQD3vn z&lEjZH)d?JinQ**hCx`1hBZ-HZb?4Z8|8d%4tX!t3oin=$gzP~Rxg$b#fXsIJFjvb z-lLCKFQr}-f8e-y7L7(}O>Di~UH9KJ|om{gyZ|KP?7Z*`jm(x7*&5$h% z9+>}hm2{S#n681iGKghd9wTDM22C(7Wa-nqKb0y%=K}7VMq`F+(Cu zZFUZUAe4hqXQj9G#lz&E0DKYZ#N$vj{JiYSO@vZR z_r1JfIGkPB!ptdI@uXw&&vrcDFS|Jr{AzMx*JeH6DF0g@2ug~tVP3CSGrM&QZFev~ zb-Y?%d-?i`vv`6^VJ>rRmQr1`;iIH(MNX`XZUPw9)oh) zc&Ue|6F!#>OPY^@gNLs5?G*Cx*CBgBoiIA4FAkm&;J2tOb$d!h@`~1C5R`{+t>Dj< z?{F&LgY?)&i#=Oa7GKpr6gx?G&y&WDTgsXL2g4gsoO3;zCMsk{2>Rs)t~|eNazB5H zT{d0-ZHy`?bOc4w+&JWV-t_|m)&_aj+p9(u^ir2vyL=&*>)-a`N9_=Pb5*422k}3} zR^JzN53I}0sUm#C8p! zxSm00>#ozWgV0Z>l$oDt70@?s1giRq`8D#OqMh%Yqja&013o(VH8g`p5SGiV2%>(6 zB`d3(+oVu4=JsK|J5s&+cR&6nPyXuCm7p%4gAVv(Wre*6=m!UkQ(T65{px5-uMJ<+ zU!$En_k;B@fib~}Y(U8Dz4zf7I$X0ey-3UTWEKv=Pv~PGA~t_B)EN!^UrhS{EaP!~ z-o)d;e%F}(*^{h~*lEFIsabO$B2E{>Uto@Dc?M@%pPa!iaB}rZOy6E#(GeMS`u60F5%? zzxnF<*llq~2(-DmYtW7hpE-vTKQJJ)GgwG7dU^L})9o>)X~!X-&x&LevG8WQInq?G z;vmAlPi_}!`ZQ$5ba^l|;*J;$T|e7zjCTB!VTcgFx{w?;%GNoxPtt6xA2{>~%)86~ z70TM|215I*^Xkq=PjTZ3+OW;b?D{w^K+5t z4ex6h8L@=lPpY&Du98g6*VUf+Mo#Htg@ep}N`af&KR&RVr}Vide{cG*W#c7I^IK<% zvqr$l5(lTT-uwDiU&os3n!b?z_JEwM-LzRce9M8@b5h4P1?<3bU`sQIeO{y5Lci zae}g$_>)E7{2xI(N?1_;YQsO<>5ysN!mED$eNOMqhdAWsAKfDJ5!JkNEDvi)erUXT z%mLi`n%Yer%5MY(m8Wf#EBu@r+^5iSs;37Fne^D7?A?~;Ip!Uc&-4x@yBC?DX)wm7 z{+WLNw~FbZp-Z)E_dekK+aa0Oc4480IMSJUBCEC!^JMjC45bne31df7!$z62#wM<= zuFB!QW{|#`{*x!tS)q8C7vI|u90?)NUf&Zq3W#P&dIn(W7hP46gr1K@lUIp{=&s&A zX!f9Lm0@v&#yQK_{Lfr~z7Wo4`+}k9d85dl0(a6#nmjmn3tyNcRXRD~4RI69qjbQT zbqNp(=QJhi>Ocp}QyYvXCv~qu`|u!W#MjW2pi@!c_wxFx>5JOQ^bAJotwwE0M#EXx zy73#>vNyjqLN@Jwm7XHFB_;{-#WAN=wY_?E9qy_b9Gn6fXUjEMCZM;~b!Q<+U`gz8 zmv?JP?orSie|7bnv&YYMpXPy;z869l6a_(e9hXI4c;7pRZGi{3UFDU{tk6 z4Q_s_%@xwzeNU|{)GxKYdbB%AIn1oM|6EjiM{vvY%Y)OcrrPPAtN{WoL5IP7d85XR zCSIuT$^c(3ZeH7Q4<6cYn9`Jj+j+Y@n%>Whl{mE+3A1dp z--3=}ZrzU##A2ZU&XRk}M49v>A+Twu`tHkegzZwHy`2a#r)4 zJr9&i8Un)i6{$UVal+5Giv+}f7j(Q*s7^;tQuoVA@Qc+I9j&ud=ixf2~CW#k^ z^(nWo3c927kj-U(jzbn;(Udho@5H2J&(`}>ahwprfGT0v>1P{B!e)K^vn=UG4Tk)a zC#&Dhu_<({!5eCAAvfMZzo!KL%z!n6tkQE3JHE4SbJ$TNL`eJ~(D?;g>-MgIRA37A zoE};uf_{n#n?w4x!iqHdSFCS{wY=7B@41-)ykv%ekH35&PboQKC&k2(dhEu484e*s zd7JC^~bF-<#oDZvlssO0RSe9EAHtA0RIe z3(9Jsu_5$9S$R2(CU5d{Z}{_XnKSd>(Z>$RQNTP%SD#3w$L9dV=2``<<}I#lY7SkA z>qte9x$p>1SKop;-1cpG-q~~CjOFB?b%)5N53x=`^|)Qvo!-Ih5}EMFhZ}9V*k=9v z=rcZFZozcBk>ABOFIoH!40#PVDV2sQ?Ab>%6aD$UgXogouPbXvXgcB8IQ*YwwEq9m zY>}=D^_f&UPWAJP*^|Kh@jc{JeaNh0h?)#wr^LC|MOWlM)IrHf&mqb2S7*@UV@Dzm z(~f7}1kG#h98hEvv)xj9Ol^YWE%64V=q>;sf`85ul-|aHJ1$mbAO&&C4-~|&hvwJ; zrxxe7m8- zND4J~or*bLgt+>s9+p(h8`X|-zbi^2ZZ#m?oujE)o?E?OzrigZ9h+SD!E5>(=IQ;V zbF$Dp9`ys}Jz7$La)N?RI-HR1nRy(e@CgwU&YCmO??-f=rSH|cfB@#E_RQGUdnS2M z4HD8?Bbifa7#|zt(&f33_3^SPf#o*bq~HZSXp3{pp&RhxZr^IsymyQ&%$F|~gh|f} z)g38#UOjxEwpEQ?e^ns`z`6{pjB>f4YI)}cQz*pH#uCo;dd(xt!`8u1c)n$4@aTP` z>6aG__>YPBS4*8QTw`#_$WGGD4H@r_LSbk-z)_FEsKu*N3iI=KS{h9q22sCWyFt-; z@;O?6BBW|QA}AV6%X|HLA`$wU;{fK0EDsSRc(tjykG2z3_CRWbUkT8;4n1ybX`ZTQ zOrPhtXwiuSq>TH;#eQ#P-dR$@wjj(M5{(AJYakhB3jCe`=Sb4AL(z+5=ZGnhoRSL7 z4H}%44bk%4!K#BNh9l`GD&|zI9%=e_Ec1^q)ZMdkjN`FTxr8eawuEP&9fzY2T543@ zJd%bc3tCJM-@CaoP4@(gXTosY2~Px*1rk^G1DIwx)|s=mGBKpYx2HhNqeN=klXEf{ zD{1gqNKITG9_bQ1gZ!7=*(wd&Yy{^WcVO!|mAB{3uhX#0ZGvimwtUx&$39(=RV-xx z?3RNaob-60s-yvo6Ct4gm18^C%D1p|yTp6uITSm02R}bH)CX;Y?~Kdn=w9`#fcYS99NiOEL!+Mms1|a0l7)rMddsMoLR^biN`<_D6Nwi# zglYT+d0Uf!K#oB1(*Y~HE(53|Ibz=IBKZ_rvGW3l6YA3}8P%c}+L0xlKBbZsLnu}e zBU#|#DIQymE87fN30SLiqd1>j(OF6sW1J2{fCZz)hzJ z#8}7=@QS?l{1!(HcA*e5ePyblbH_e8WMHcbxMMvG#Jt z3lFX;&YyrNO7VCZ&p3{M&xCUW+Y?Y@Su+;A!qsMB31M@6ftpLH=1{djd+gh@y()a3 zKwGD5v;)X{epn?Nxyyd@FIrVI5)Y&_CTs5z#Zwvg!GkdveZX>Wy7d|?_zGPQC^B`> zIs2}3Mo>eBNZCVZ%cVt_esf`d*pO591N2NH!9!jz%6$oQhDp>%Zum(^cz{bY%Ah+> z;PNbRrk0L}8I*z)7J^TM zJUYSRK$mkkxSG}EC^fcS&dE5)WvZSo2TBJ8gXtWbHA#NO_NzCD-GG{I8_P&X7j}7` zQQU6cuK9O%BH8K)W za<;7xxaW`pR~YAjx)sEtQBFtUuQpglPWQKW7B_okq$HtJ2w2bKWDnIaxQ%oOU5xJK z-Mvy>*y#!rpW9X@b-9AtW~I-(@V0&!$RaIi5!0~;I6|_6e_1f{&7k0nLF_(VtHB1i z%mTk`nt)rnsQb}#ln8t8P(Q7jj89dK5&NP|ZkDol8{O&L)4k0nwdfj8ho7yL=%yHk z*f$llxzT($^h;qv1+YrB>XLp3_k&I&hQ)0lNw; zK>d=lK8VJnsS`r>Dopq9O({#q)yzWgPKUvvRW_EJ6pM8Ts4_2DY+S&4b>)(|r6mEH z?XMm^p2qM(tQ;JY`p+re#G+cnLTH5APz|ZfxI)jGqa>&8aobsep%Y~zJ~+;IQA#P; zvgwA#SS&DLEmn7xHEM<1Dm#*2|IBj(f7*Nwzcl_>Esvvc;$(@xm1r5lmI;-vHYT?C z`-TJ<&-Ve2bJJt{7PIm6Cy%A~pJV%^6DQGhz*FC#> zy$ua9nk_E{Fh<%KEot;fT*Y{SXSx|_JAyB0ZVfjtlc1XxaP-y(K?^mcjx3E0^gL67 zBj&nY1yB{Z1!~v(*QuD*5R{R4_EI0=xSg@;mM^a=ShW^<3LvTbH4=ZYi`-^c z-BUMTBtlz_6f`rC$9El_(*>=NVcQ8{k0Z49h&>>O+Z!&xxuxU+_pOC!CIcUfL{YnX zAp-2X)2Z+rXok>f-hH}$IGNaDxiq(#hBf3r4hQyFC~Vxjjj|$?c=QxpiWpSnnAKkk zPWbV(V|g(9d$xbYfr3RnD<~;GiZVjL;Dl1+1cjaN?mzbL-@C-0AE{hf%tGfaw3ocO z%_{XsV|-5FtNjcn2++wvJ*$qHafXX|Om$Xkktf8;Dzl)p)5ozaP_({gsXy8Cp)`Z8 zlD>YvCindMx@djK`!4I7&UcMj&iR$e;a(hJ;%c;b@AD$Fd++Uj-Q9ib=PR4sm^A&? z9QLL-?KhH6fV5hY0&)+72^n|dksDJH>5=lFOcx%@0U#_i1xPj_08lk{7A$zvOzR^~ zPW1Nn-A<1~-(KVmG1cpbqwwOqc_;FeKeD{hVdUWZS_5jreQ@bOo$&NGl2=>0-e$vd z4}B?ME4G0-qE?dT?dvNwOLLu;K&50(>nyUDQ}%Y~k~o)!z}a#ySyt3J6|;Um?=S## zVYUDGI!~F{ch+*iiEj^5_@)Y#a3)m(nA9-(+x+z0g z%^4KY8IpJzJfq=0BA$WG%ogCbGltj1{k28{m%gqmQb%$I<(_bqX zEvN(vncZd{igo-C zLG}Izl=uAw8*JPnU6;SWsLhcJJqd;(r@6rpgS%mp>OgXGaiK$wxqDnXvK<#RAa-$E z0jjEFKqohh9nZ8D*=W%V`Nia{y{J^~30n#^S?nEo38bs;IZLBshTQDJYfCSswR|TO z(I&hBhiD=kiCR!J5={o(>vyg%-^FGa3FCr;6pVs~EIq(gl*ug4bs7sC&8)ZfL*Wf- z4fy~2blM6%=hrD$hPz8+g&PcK3KtPr)qNZ7;dLpIi<3bDXV~I-d5v(6JN=0@eU63> z^Z?;=y*@lh(Sq=eirqa4Ck3B2kn67;UPUJq3j)I$R?RmSFBiA?;awnj zr+4_m2LBrqWJutJ&-kLqhTgA@iZuE3xWcv!sJ3fZow;P&pM`t2+Na@Op4-S67n|lC zgB~gg3nuq^povWLt+C{J$L!Y;jx*%d^TSZ|;o%FNE${;#?r&8&ns`|nb#~Ga%;RS-&~NeykeuzubXR62e7^iA56--R|)% zY~Ie6*F1SEPD4=|t{AYo`L%|v8ys%PRn>AD?OHPWuSiSvD+Q@7kC;$YenX)y&SIxN zg3iF8b1}*)*$*kSGcjPD`XM#AI$-586jzDA}yZNyR zZ74N7_1A*syH{G8OV7q|J@FOaAsZW+dR>mfL8-}V7NcA>4Hyb8oUe-|Og~vQKQ!Ga z*+xLoxvL7o&NA)NHw7q?L~{e@2nv!YiQtM=GfCjIG@>|An4}V>gc@ym1tkqMcYqlS z#SH}WNEC1-prk~E$;^ycdJZ>vZWq|+|MD{mtzwzyWtDG$0Wbt^^kLd~)qG?RkwvCq zkrM*d8x6Cme7@(Q%4HU+qkw1|ike+oZ@3#QE8sC2cMN29p;)6D0Ya18k^7u|?X%h_ z#;v!tmS%zhhpkz5{$M?l;ud}kdjE6;yc`r3t6t^{QDiD-9u4|QT|Lc)<mzXxjYZ8H_2THaggLNJX%`yamBLiN4{6-(#6iWmOW!a+mB;q}o6hC)@E+v~fZj0{xOb8urGNq+I?SK$-Nh%OB}EVJfk7lm!W z5`<+Y8))aY87N1F1{H**)uDPaHn5pOVIkmjuz~Iaa6pkxrxZGW5(XVLO^hWXoq=FV zS8o3LIqRascc?6#sJXg%sU86)tJihs2lhR`Y&p!4EVn{*J3N|LDJ=V(5(7>( z&lA5i-@s=KK+9GA&JiVq;$h3?db5BZ#V%*|R!P8qq#?l^coOyg57ldpw|3ss37gpr zSm`Nrd5K>cOvox>TnE0HM9q@Ez2HDNr(06W9~nT7YdFMR<7fCwQ-IHU zqKZ2N?m~9p+Rl+>C|3FMfL7(COIsO{UZiBxu*qR&hpz{kcoLjwZw|~{wj75B>5B+x zAwi2)h@=@HtIl1*Sec2!GJ#dYkz{d@2B?8KO)@YzkHM^F5f1LR)=kZL1UO+)WD9KH z)H%Lk_G~G1Cw6Op0MTeW$EmiUQENIDl^oHaHP9xXWI>0Wygu@BQDHHP+m3YSTR|~k zjNA+n#)Hwr`9uyv(p$RqcJBLomWORQ79%fJ@k#NT4z_bO%RfsD2cJSo;V#I$beqkI zrQIj;7O_l-)F<{0Kae5@Ku(nBs|dAPNHVyyhOP&J+j)Bp1xh)SA`a0Sglc~|#y$#r z&Tz0y-7d!j7_@bkGzlYM^GJs zizD?zG}9GLchpeXX~XP`e8sKDfNA4;%urp5qI^$m!A)DVDz76s3Utrdc5yI6Z@=E+9xNoqu(FC>W6gCBlmh3ityU_}gP&E9UW9PPBV5D~_=pUnB8K z3*NK3`+0xpU~c%$MoksNxo(cXE#JO-ripSVIceoy@}O3Cdhb24699Osz{qnXYQyHP z@*2pKK&LLaN;eV{YqX9=h`2wGw5C-v^8rq@1m5`Lz+%@^4AF_o>4v+$`9HICQx&I* z;lPJFfi&}^UgYByEQ3C_fSb00)7MZ8x(nsE^LvXdp&?4=9v*wiF!-U*boF{BAD@u0 ze2Lx`5*0m`XZU}103$vg5ST7P4O)BWoU`i^r!ETXQ`LMdNzV_3s^-O4A@b?pUwpzf zoY&&i*=-97G!@sC?CCU*Y|{@cKoAA{+pui>cQgDi-$I&pRLQW>Xe7Ox_@qPmf``Y? z2g;<}BTv-KArN)JmR)_5KYxVQ1&L(MW;ITYBdvv14UYWMEKd_VD3++ Ge*0fc! Date: Fri, 16 Jan 2026 23:59:06 -0500 Subject: [PATCH 3/4] fix: Add missing pages to MkDocs nav configuration Added to nav: - Architecture Evolution document - Design docs (tray, telemetry, landing page, etc.) - Roadmap docs (priorities, publications) - macOS permissions reference - Legacy freeze documents Fixes MkDocs strict mode warnings about orphaned pages. Co-Authored-By: Claude Sonnet 4.5 --- mkdocs.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 39e4c9985..c0b353060 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -116,8 +116,23 @@ nav: - openadapt-grounding: packages/grounding.md - openadapt-retrieval: packages/retrieval.md - openadapt-privacy: packages/privacy.md - - Architecture: architecture.md + - Architecture: + - Overview: architecture.md + - Evolution: architecture-evolution.md + - Design: + - Index: design/INDEX.md + - System Tray App: design/openadapt-tray.md + - Tray Logging: design/tray-logging.md + - Telemetry: design/telemetry-design.md + - Landing Page: design/landing-page-strategy.md + - Repo Rename Analysis: design/repo-rename-analysis.md + - Roadmap: + - Priorities: roadmap-priorities.md + - Publications: publication-roadmap.md - CLI Reference: cli.md - Contributing: contributing.md - Legacy: - Legacy Freeze: legacy/freeze.md + - Legacy Freeze (Alt): LEGACY_FREEZE.md + - Reference: + - macOS Permissions: permissions-macos.md From 71bdc10dec0d665dd7a0a350327fdcb95030ce8f Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Fri, 16 Jan 2026 23:59:37 -0500 Subject: [PATCH 4/4] chore: Update author email to richard@openadapt.ai Co-Authored-By: Claude Sonnet 4.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b737bbb04..e27f1f34a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" license = "MIT" authors = [ - {name = "MLDSAI Inc.", email = "richard@mldsai.com"} + {name = "Richard Abrich", email = "richard@openadapt.ai"} ] keywords = ["gui", "automation", "ml", "rpa", "agent", "vlm", "computer-use"] classifiers = [