Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ ASKUI_WORKSPACE_ID=
TARS_URL=
TARS_API_KEY=

# OpenRouter
OPEN_ROUTER_API_KEY=

# Telemetry
ASKUI__VA__TELEMETRY__ENABLED=True # Set to "False" to disable telemetry
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,53 @@ with VisionAgent(models=custom_models, model="dynamic-model") as agent:
agent.act("do something", model="dynamic-model-cached") # reuses cached instance
```

### 🔀 OpenRouter **AI Models**

You can use Vision Agent with [OpenRouter](https://openrouter.ai/) to access a wide variety of models via a unified API.

**Set your OpenRouter API key:**

<details>
<summary>Linux & MacOS</summary>

```shell
export OPEN_ROUTER_API_KEY=<your-openrouter-api-key>
```
</details>
<details>
<summary>Windows PowerShell</summary>

```shell
$env:OPEN_ROUTER_API_KEY="<your-openrouter-api-key>"
```
</details>

**Example: Using OpenRouter with a custom model registry**

```python
from askui import VisionAgent
from askui.models import (
OpenRouterGetModel,
OpenRouterSettings,
ModelRegistry,
)


# Register OpenRouter model in the registry
custom_models: ModelRegistry = {
"my-custom-model": OpenRouterGetModel(
OpenRouterSettings(
model="anthropic/claude-opus-4",
)
),
}

with VisionAgent(model_registry=custom_registry, model={"get":"my-custom-model"}) as agent:
agent.click("search field")
result = agent.get("What is the main heading on the screen?")
print(result)
```


### 🛠️ Direct Tool Use

Expand Down
2 changes: 1 addition & 1 deletion src/askui/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from askui.models.shared.computer_agent_message_param import MessageParam
from askui.utils.image_utils import ImageSource, Img

from .exceptions import ElementNotFoundError
from .logger import configure_logging, logger
from .models import ModelComposition
from .models.exceptions import ElementNotFoundError
from .models.model_router import ModelRouter
from .models.models import (
ModelChoice,
Expand Down
111 changes: 8 additions & 103 deletions src/askui/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,108 +1,13 @@
from typing import Any

from askui.locators.locators import Locator

from .models.askui.ai_element_utils import AiElementNotFound
from .models.askui.exceptions import AskUiApiError, AskUiApiRequestFailedError


class AutomationError(Exception):
"""Exception raised when the automation step cannot complete.

Args:
message (str): The error message.
"""

def __init__(self, message: str):
self.message = message
super().__init__(self.message)


class ElementNotFoundError(AutomationError):
"""Exception raised when an element cannot be located.

Args:
locator (str | Locator): The locator that was used.
locator_serialized (Any): The locator serialized for the specific model
"""

def __init__(self, locator: str | Locator, locator_serialized: Any) -> None:
self.locator = locator
self.locator_serialized = locator_serialized
super().__init__(f"Element not found: {self.locator}")


class ModelNotFoundError(AutomationError):
"""Exception raised when a model could not be found within available models.

Args:
model_choice (str): The model choice.
"""

def __init__(
self,
model_choice: str,
message: str | None = None,
):
self.model_choice = model_choice
super().__init__(
f"Model not found: {model_choice}" if message is None else message
)


class ModelTypeMismatchError(ModelNotFoundError):
"""Exception raised when a model is not of the expected type.

Args:
model_choice (str): The model choice.
expected_type (type): The expected type.
actual_type (type): The actual type.
"""

def __init__(
self,
model_choice: str,
expected_type: type,
actual_type: type,
):
self.expected_type = expected_type
self.actual_type = actual_type
super().__init__(
model_choice=model_choice,
message=f'Model "{model_choice}" is an instance of {actual_type.mro()}, '
f"expected it to be an instance of {expected_type.mro()}",
)


class QueryNoResponseError(AutomationError):
"""Exception raised when a query does not return a response.

Args:
message (str): The error message.
query (str): The query that was made.
"""

def __init__(self, message: str, query: str):
self.message = message
self.query = query
super().__init__(self.message)


class QueryUnexpectedResponseError(AutomationError):
"""Exception raised when a query returns an unexpected response.

Args:
message (str): The error message.
query (str): The query that was made.
response (Any): The response that was received.
"""

def __init__(self, message: str, query: str, response: Any):
self.message = message
self.query = query
self.response = response
super().__init__(self.message)

from .models.exceptions import (
AutomationError,
ElementNotFoundError,
ModelNotFoundError,
ModelTypeMismatchError,
QueryNoResponseError,
QueryUnexpectedResponseError,
)

__all__ = [
"AiElementNotFound",
Expand Down
4 changes: 4 additions & 0 deletions src/askui/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
OnMessageCb,
Point,
)
from .openrouter.handler import OpenRouterGetModel
from .openrouter.settings import OpenRouterSettings
from .shared.computer_agent_message_param import (
Base64ImageSourceParam,
CacheControlEphemeralParam,
Expand Down Expand Up @@ -52,4 +54,6 @@
"ToolResultBlockParam",
"ToolUseBlockParam",
"UrlImageSourceParam",
"OpenRouterGetModel",
"OpenRouterSettings",
]
10 changes: 5 additions & 5 deletions src/askui/models/anthropic/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import anthropic
from typing_extensions import override

from askui.exceptions import (
ElementNotFoundError,
QueryNoResponseError,
QueryUnexpectedResponseError,
)
from askui.locators.locators import Locator
from askui.locators.serializers import VlmLocatorSerializer
from askui.logger import logger
from askui.models.anthropic.settings import ClaudeSettings
from askui.models.exceptions import (
ElementNotFoundError,
QueryNoResponseError,
QueryUnexpectedResponseError,
)
from askui.models.models import (
ANTHROPIC_MODEL_NAME_MAPPING,
GetModel,
Expand Down
2 changes: 1 addition & 1 deletion src/askui/models/askui/inference_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from pydantic import RootModel
from typing_extensions import override

from askui.exceptions import ElementNotFoundError
from askui.locators.locators import Locator
from askui.locators.serializers import AskUiLocatorSerializer, AskUiSerializedLocator
from askui.logger import logger
from askui.models.askui.settings import AskUiSettings
from askui.models.exceptions import ElementNotFoundError
from askui.models.models import GetModel, LocateModel, ModelComposition, Point
from askui.models.types.response_schemas import ResponseSchema
from askui.utils.image_utils import ImageSource
Expand Down
6 changes: 5 additions & 1 deletion src/askui/models/askui/model_router.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing_extensions import override

from askui.exceptions import AutomationError, ElementNotFoundError, ModelNotFoundError
from askui.locators.locators import AiElement, Locator, Prompt, Text
from askui.logger import logger
from askui.models.askui.inference_api import AskUiInferenceApi
from askui.models.exceptions import (
AutomationError,
ElementNotFoundError,
ModelNotFoundError,
)
from askui.models.models import LocateModel, ModelComposition, ModelName, Point
from askui.utils.image_utils import ImageSource

Expand Down
101 changes: 101 additions & 0 deletions src/askui/models/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from typing import Any

from askui.locators.locators import Locator


class AutomationError(Exception):
"""Exception raised when the automation step cannot complete.

Args:
message (str): The error message.
"""

def __init__(self, message: str):
self.message = message
super().__init__(self.message)


class QueryNoResponseError(AutomationError):
"""Exception raised when a query does not return a response.

Args:
message (str): The error message.
query (str): The query that was made.
"""

def __init__(self, message: str, query: str):
self.message = message
self.query = query
super().__init__(self.message)


class ElementNotFoundError(AutomationError):
"""Exception raised when an element cannot be located.

Args:
locator (str | Locator): The locator that was used.
locator_serialized (Any): The locator serialized for the specific model
"""

def __init__(self, locator: str | Locator, locator_serialized: Any) -> None:
self.locator = locator
self.locator_serialized = locator_serialized
super().__init__(f"Element not found: {self.locator}")


class QueryUnexpectedResponseError(AutomationError):
"""Exception raised when a query returns an unexpected response.

Args:
message (str): The error message.
query (str): The query that was made.
response (Any): The response that was received.
"""

def __init__(self, message: str, query: str, response: Any):
self.message = message
self.query = query
self.response = response
super().__init__(self.message)


class ModelNotFoundError(AutomationError):
"""Exception raised when a model could not be found within available models.

Args:
model_choice (str): The model choice.
"""

def __init__(
self,
model_choice: str,
message: str | None = None,
):
self.model_choice = model_choice
super().__init__(
f"Model not found: {model_choice}" if message is None else message
)


class ModelTypeMismatchError(ModelNotFoundError):
"""Exception raised when a model is not of the expected type.

Args:
model_choice (str): The model choice.
expected_type (type): The expected type.
actual_type (type): The actual type.
"""

def __init__(
self,
model_choice: str,
expected_type: type,
actual_type: type,
):
self.expected_type = expected_type
self.actual_type = actual_type
super().__init__(
model_choice=model_choice,
message=f'Model "{model_choice}" is an instance of {actual_type.mro()}, '
f"expected it to be an instance of {expected_type.mro()}",
)
2 changes: 1 addition & 1 deletion src/askui/models/model_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from typing_extensions import Literal

from askui.exceptions import ModelNotFoundError, ModelTypeMismatchError
from askui.locators.locators import Locator
from askui.locators.serializers import AskUiLocatorSerializer, VlmLocatorSerializer
from askui.models.anthropic.settings import (
Expand All @@ -15,6 +14,7 @@
from askui.models.askui.computer_agent import AskUiComputerAgent
from askui.models.askui.model_router import AskUiModelRouter
from askui.models.askui.settings import AskUiComputerAgentSettings
from askui.models.exceptions import ModelNotFoundError, ModelTypeMismatchError
from askui.models.huggingface.spaces_api import HFSpacesHandler
from askui.models.models import (
MODEL_TYPES,
Expand Down
1 change: 1 addition & 0 deletions src/askui/models/openrouter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""OpenRouter model implementations."""
Loading