notte-integrations 0.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. notte_integrations-0.0.dev0/.gitignore +179 -0
  2. notte_integrations-0.0.dev0/PKG-INFO +17 -0
  3. notte_integrations-0.0.dev0/README.md +0 -0
  4. notte_integrations-0.0.dev0/pyproject.toml +33 -0
  5. notte_integrations-0.0.dev0/src/notte_integrations/__init__.py +3 -0
  6. notte_integrations-0.0.dev0/src/notte_integrations/api/fastapi.py +34 -0
  7. notte_integrations-0.0.dev0/src/notte_integrations/credentials/README.md +37 -0
  8. notte_integrations-0.0.dev0/src/notte_integrations/credentials/__init__.py +0 -0
  9. notte_integrations-0.0.dev0/src/notte_integrations/credentials/hashicorp/__init__.py +0 -0
  10. notte_integrations-0.0.dev0/src/notte_integrations/credentials/hashicorp/docker-compose.yml +15 -0
  11. notte_integrations-0.0.dev0/src/notte_integrations/credentials/hashicorp/vault.py +174 -0
  12. notte_integrations-0.0.dev0/src/notte_integrations/notifiers/README.md +129 -0
  13. notte_integrations-0.0.dev0/src/notte_integrations/notifiers/__init__.py +0 -0
  14. notte_integrations-0.0.dev0/src/notte_integrations/notifiers/discord.py +41 -0
  15. notte_integrations-0.0.dev0/src/notte_integrations/notifiers/mail.py +67 -0
  16. notte_integrations-0.0.dev0/src/notte_integrations/notifiers/slack.py +25 -0
  17. notte_integrations-0.0.dev0/src/notte_integrations/py.typed +0 -0
  18. notte_integrations-0.0.dev0/src/notte_integrations/sessions/__init__.py +0 -0
  19. notte_integrations-0.0.dev0/src/notte_integrations/sessions/anchor.py +57 -0
  20. notte_integrations-0.0.dev0/src/notte_integrations/sessions/browserbase.py +82 -0
  21. notte_integrations-0.0.dev0/src/notte_integrations/sessions/cdp_session.py +77 -0
  22. notte_integrations-0.0.dev0/src/notte_integrations/sessions/steel.py +53 -0
@@ -0,0 +1,179 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ ignore.*
171
+ llm_usage.jsonl
172
+ llm_parsing_error.jsonl
173
+ traces/
174
+
175
+ **/__pycache__/**
176
+ .DS_Store
177
+ **/.DS_Store
178
+ old
179
+ notebook
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: notte-integrations
3
+ Version: 0.0.dev0
4
+ Summary: The integrations for Notte
5
+ Author-email: Notte Team <hello@notte.cc>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: browser-use>=0.1.40
8
+ Requires-Dist: browserbase>=1.2.0
9
+ Requires-Dist: discord-py<2.5.0,>=2.3.0
10
+ Requires-Dist: fastapi>=0.115.8
11
+ Requires-Dist: hvac>=2.3.0
12
+ Requires-Dist: langchain-google-genai>=2.1.1
13
+ Requires-Dist: notte-agent==0.0.dev
14
+ Requires-Dist: notte-browser==0.0.dev
15
+ Requires-Dist: notte-core==0.0.dev
16
+ Requires-Dist: slack-sdk>=3.34.0
17
+ Requires-Dist: uvicorn>=0.29.0
File without changes
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "notte-integrations"
3
+ version = "0.0.dev"
4
+ description = "The integrations for Notte"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Notte Team ", email = "hello@notte.cc" }
8
+ ]
9
+ packages = [
10
+ { include = "notte_integrations", from = "src" },
11
+ ]
12
+
13
+
14
+ requires-python = ">=3.11"
15
+ dependencies = [
16
+ "notte_core==0.0.dev",
17
+ "notte_browser==0.0.dev",
18
+ "notte_agent==0.0.dev",
19
+ "discord-py>=2.3.0,<2.5.0",
20
+ "fastapi>=0.115.8",
21
+ "uvicorn>=0.29.0",
22
+ "hvac>=2.3.0",
23
+ "browserbase>=1.2.0",
24
+ "browser-use>=0.1.40",
25
+ "langchain-google-genai>=2.1.1",
26
+ "slack-sdk>=3.34.0",
27
+ ]
28
+
29
+
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
@@ -0,0 +1,3 @@
1
+ from notte_core import check_notte_version
2
+
3
+ __version__ = check_notte_version("notte_integrations")
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from notte_agent.common.base import BaseAgent
7
+ from notte_agent.common.types import AgentResponse
8
+ from notte_sdk.types import AgentRunRequest
9
+
10
+
11
+ def create_agent_router(agent: BaseAgent, prefix: str = "agent") -> APIRouter:
12
+ """
13
+ Creates a FastAPI router that serves the given agent.
14
+
15
+ Args:
16
+ agent: The BaseAgent implementation to serve
17
+ prefix: Optional URL prefix for the API endpoints
18
+
19
+ Returns:
20
+ APIRouter instance with agent endpoints
21
+ """
22
+ router = APIRouter(
23
+ prefix=prefix,
24
+ tags=[agent.__class__.__name__],
25
+ )
26
+
27
+ @router.post("/run", response_model=AgentResponse)
28
+ async def run_agent(request: Annotated[AgentRunRequest, "Agent request parameters"]) -> AgentResponse: # pyright: ignore[reportUnusedFunction]
29
+ try:
30
+ return await agent.run(task=request.task, url=request.url)
31
+ except Exception as e:
32
+ raise HTTPException(status_code=500, detail=str(e))
33
+
34
+ return router
@@ -0,0 +1,37 @@
1
+ ## Basic example code
2
+
3
+ ```python
4
+ from notte_agent.main import Agent
5
+ from notte_integrations.credentials.hashicorp.vault import HashiCorpVault
6
+ import os
7
+ from dotenv import load_dotenv
8
+
9
+
10
+ # CRITICAL: make sure to start the vault server before running the agent
11
+ # > cd packages/notte-integrations/src/notte_integrations/credentials/hashicorp
12
+ # > docker-compose --env-file ../../../../../.env up
13
+
14
+ # Then, set the VAULT_URL environment variable either to .env file or
15
+
16
+ # VAULT_URL=http://0.0.0.0:8200
17
+ # VAULT_DEV_ROOT_TOKEN_ID=<your-vault-dev-root-token-id>
18
+
19
+ # Load environment variables
20
+ load_dotenv()
21
+
22
+ # Initialize vault with environment variables
23
+ vault = HashiCorpVault(
24
+ url=os.getenv("VAULT_URL"),
25
+ token=os.getenv("VAULT_DEV_ROOT_TOKEN_ID")
26
+ )
27
+
28
+ # Add credentials from environment variables
29
+ vault.add_credentials(
30
+ url="https://x.com",
31
+ username=os.getenv("TWITTER_USERNAME"),
32
+ password=os.getenv("TWITTER_PASSWORD")
33
+ )
34
+
35
+ # Configure and initialize agent with vault
36
+ agent = Agent(vault=vault)
37
+ ```
@@ -0,0 +1,15 @@
1
+ version: '3.8'
2
+ services:
3
+ vault:
4
+ image: hashicorp/vault:latest
5
+ container_name: vault
6
+ ports:
7
+ - "8200:8200"
8
+ environment:
9
+ - VAULT_DEV_ROOT_TOKEN_ID=${VAULT_DEV_ROOT_TOKEN_ID}
10
+ - VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200
11
+ cap_add:
12
+ - IPC_LOCK
13
+ command: server -dev
14
+
15
+ # run it using docker-compose --env-file /path/to/.env up
@@ -0,0 +1,174 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from typing import Any, Protocol, final
4
+
5
+ from hvac.exceptions import InvalidPath
6
+ from notte_core.credentials.base import (
7
+ BaseVault,
8
+ CredentialField,
9
+ VaultCredentials,
10
+ )
11
+ from notte_core.utils.url import get_root_domain
12
+ from typing_extensions import override
13
+
14
+
15
+ class SysProtocol(Protocol):
16
+ def list_mounted_secrets_engines(self) -> Any: ...
17
+ def enable_secrets_engine(self, backend_type: str, path: str, options: dict[str, Any]) -> Any: ...
18
+
19
+
20
+ class SecretsProtocol(Protocol):
21
+ def read_secret_version(self, path: str, mount_point: str) -> Any: ...
22
+ def create_or_update_secret(self, path: str, secret: dict[str, Any], mount_point: str) -> Any: ...
23
+ def delete_metadata_and_all_versions(self, path: str, mount_point: str) -> Any: ...
24
+
25
+
26
+ @dataclass
27
+ class HashiCorpVaultClientProtocol:
28
+ url: str
29
+ token: str
30
+ sys: SysProtocol
31
+
32
+
33
+ try:
34
+ from hvac import Client as HashiCorpVaultClient
35
+
36
+ VAULT_AVAILABLE = True
37
+ except ImportError:
38
+ VAULT_AVAILABLE = False # type: ignore
39
+
40
+
41
+ def check_vault_imports():
42
+ if not VAULT_AVAILABLE:
43
+ raise ImportError(
44
+ (
45
+ "The 'hvac' package is required for HashiCorp Vault integration."
46
+ " Install 'vault' optional dependencies with 'uv sync --extra vault'"
47
+ )
48
+ )
49
+
50
+
51
+ @final
52
+ class HashiCorpVault(BaseVault):
53
+ """HashiCorp Vault implementation of the BaseVault interface."""
54
+
55
+ def __init__(self, url: str, token: str):
56
+ check_vault_imports()
57
+ self.client: HashiCorpVaultClientProtocol = HashiCorpVaultClient(url=url, token=token) # type: ignore
58
+ self.secrets: SecretsProtocol = self.client.secrets.kv.v2 # type: ignore
59
+ self._mount_path: str = "secret"
60
+ self._init_vault()
61
+
62
+ def _init_vault(self) -> None:
63
+ try:
64
+ mounts = self.client.sys.list_mounted_secrets_engines()
65
+ if self._mount_path not in mounts["data"]:
66
+ self.client.sys.enable_secrets_engine(
67
+ backend_type="kv", path=self._mount_path, options={"version": "2"}
68
+ )
69
+ else:
70
+ mount_info = mounts["data"][f"{self._mount_path}/"]["options"]
71
+ if mount_info.get("version") != "2":
72
+ raise ValueError(f"Existing {self._mount_path} mount is not a KV v2 secrets engine")
73
+ except Exception as e:
74
+ if "path is already in use" not in str(e):
75
+ raise e
76
+
77
+ @override
78
+ def _set_singleton_credentials(self, creds: list[CredentialField]) -> None:
79
+ for cred in creds:
80
+ if not cred.singleton:
81
+ raise ValueError(f"{cred.__class__} can't be set as singleton credential: url-specific only")
82
+
83
+ self.secrets.create_or_update_secret(
84
+ path="singleton_credentials",
85
+ secret=dict(
86
+ **{cred.__class__.__qualname__: cred.value for cred in creds},
87
+ ),
88
+ mount_point=self._mount_path,
89
+ )
90
+
91
+ @override
92
+ def get_singleton_credentials(self) -> list[CredentialField]:
93
+ try:
94
+ secret = self.secrets.read_secret_version(path="singleton_credentials", mount_point=self._mount_path)
95
+ except InvalidPath:
96
+ return []
97
+
98
+ data = secret["data"]["data"]
99
+
100
+ return [CredentialField.registry[key](value=value) for key, value in data.items()]
101
+
102
+ @override
103
+ def _add_credentials(self, creds: VaultCredentials) -> None:
104
+ for cred in creds.creds:
105
+ if cred.singleton:
106
+ raise ValueError(f"{cred.__class__} can't be set as url specific credential: singleton only")
107
+ domain = get_root_domain(creds.url)
108
+ self.secrets.create_or_update_secret(
109
+ path=f"credentials/{domain}",
110
+ secret=dict(
111
+ url=creds.url,
112
+ **{cred.__class__.__qualname__: cred.value for cred in creds.creds},
113
+ ),
114
+ mount_point=self._mount_path,
115
+ )
116
+
117
+ @override
118
+ def _get_credentials_impl(self, url: str) -> VaultCredentials | None:
119
+ domain = get_root_domain(url)
120
+ try:
121
+ secret = self.secrets.read_secret_version(path=f"credentials/{domain}", mount_point=self._mount_path)
122
+ data = secret["data"]["data"]
123
+ url = data["url"]
124
+ del data["url"]
125
+
126
+ return VaultCredentials(
127
+ url=url,
128
+ creds=[CredentialField.registry[key](value=value) for key, value in data.items()],
129
+ )
130
+
131
+ except Exception:
132
+ return None
133
+
134
+ @override
135
+ def remove_credentials(self, url: str) -> None:
136
+ domain = get_root_domain(url)
137
+ self.secrets.delete_metadata_and_all_versions(path=f"credentials/{domain}", mount_point=self._mount_path)
138
+
139
+ @classmethod
140
+ def create_from_env(cls) -> "HashiCorpVault":
141
+ """Create a HashiCorpVault instance from environment variables.
142
+
143
+ Requires VAULT_URL and VAULT_DEV_ROOT_TOKEN_ID to be set in environment variables.
144
+ Automatically loads from .env file if present.
145
+
146
+ Returns:
147
+ HashiCorpVault: Initialized vault instance
148
+
149
+ Raises:
150
+ ValueError: If required environment variables are missing or vault server is not running
151
+ """
152
+ vault_url = os.getenv("VAULT_URL")
153
+ vault_token = os.getenv("VAULT_DEV_ROOT_TOKEN_ID")
154
+
155
+ if not vault_url or not vault_token:
156
+ raise ValueError(
157
+ """"
158
+ VAULT_URL and VAULT_DEV_ROOT_TOKEN_ID must be set in the .env file.
159
+ For example if you are running the vault locally:
160
+ VAULT_URL=http://0.0.0.0:8200
161
+ VAULT_DEV_ROOT_TOKEN_ID=<your-vault-dev-root-token-id>
162
+ """
163
+ )
164
+
165
+ try:
166
+ return cls(url=vault_url, token=vault_token)
167
+ except ConnectionError as e:
168
+ vault_not_running_instructions = """
169
+ Make sure to start the vault server before running the agent.
170
+ Instructions to start the vault server:
171
+ > cd packages/notte-integrations/src/notte_integrations/credentials/hashicorp
172
+ > docker-compose --env-file ../../../../../.env up
173
+ """
174
+ raise ValueError(f"Vault server is not running. {vault_not_running_instructions}") from e
@@ -0,0 +1,129 @@
1
+ # Notte Notifiers
2
+
3
+ This guide explains how to set up notification services for Notte using Discord and Slack integrations.
4
+
5
+ ## Table of Contents
6
+ - [Overview](#overview)
7
+ - [Slack Integration](#slack-integration)
8
+ - [Creating a Slack App](#creating-a-slack-app)
9
+ - [Getting the Required Credentials](#getting-the-slack-credentials)
10
+ - [Configuration](#slack-configuration)
11
+ - [Discord Integration](#discord-integration)
12
+ - [Creating a Discord Bot](#creating-a-discord-bot)
13
+ - [Getting the Required Credentials](#getting-the-discord-credentials)
14
+ - [Configuration](#discord-configuration)
15
+ - [Using the Notifiers](#using-the-notifiers)
16
+
17
+ ## Overview
18
+
19
+ Notte provides notification services that can send task results to both Slack and Discord. Each integration requires specific credentials that you'll need to obtain from their respective platforms.
20
+
21
+ ## Slack Integration
22
+
23
+ ### Creating a Slack App
24
+
25
+ 1. Go to [Slack API](https://api.slack.com/apps)
26
+ 2. Click "Create New App"
27
+ 3. Select "From scratch"
28
+ 4. Enter a name for your app (e.g., "Notte Notifier")
29
+ 5. Select the workspace where you want to install the app
30
+ 6. Click "Create App"
31
+
32
+ ### Getting the Slack Credentials
33
+
34
+ 1. In your app's settings page, navigate to "OAuth & Permissions" in the sidebar
35
+ 2. Scroll down to "Scopes" and add the following Bot Token Scopes:
36
+ - `chat:write`
37
+ - `chat:write.public`
38
+ 3. Scroll back to the top and click "Install to Workspace"
39
+ 4. Authorize the app installation
40
+ 5. After installation, you'll see a "Bot User OAuth Token" that starts with `xoxb-` - copy this token
41
+
42
+ To get your channel ID:
43
+ 1. Open Slack in your browser
44
+ 2. Navigate to the channel where you want to send notifications
45
+ 3. The channel ID is in the URL: `https://app.slack.com/client/TXXXXXXXX/CXXXXXXXXX`
46
+ (The `CXXXXXXXXX` part is your channel ID)
47
+
48
+ ### Slack Configuration
49
+
50
+ Use the following code to initialize the Slack notifier:
51
+
52
+ ```python
53
+ from notte_core.notifiers.slack import SlackConfig, SlackNotifier
54
+
55
+ slack_config = SlackConfig(
56
+ token="xoxb-your-token-here",
57
+ channel_id="C0123456789"
58
+ )
59
+
60
+ slack_notifier = SlackNotifier(slack_config)
61
+ ```
62
+
63
+ ## Discord Integration
64
+
65
+ ### Creating a Discord Bot
66
+
67
+ 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
68
+ 2. Click "New Application"
69
+ 3. Enter a name for your app (e.g., "Notte Notifier")
70
+ 4. Navigate to the "Bot" tab in the left sidebar
71
+ 5. Click "Add Bot"
72
+ 6. Under the "Privileged Gateway Intents" section, enable "Message Content Intent"
73
+
74
+ ### Getting the Discord Credentials
75
+
76
+ To get your bot token:
77
+ 1. In the "Bot" tab, click "Reset Token" to generate a new token
78
+ 2. Copy the token - this is your bot token
79
+
80
+ To invite the bot to your server:
81
+ 1. Go to the "OAuth2" tab and then "URL Generator"
82
+ 2. Select the following scopes:
83
+ - `bot`
84
+ 3. Select the following bot permissions:
85
+ - "Send Messages"
86
+ - "Read Message History"
87
+ 4. Copy the generated URL and open it in your browser
88
+ 5. Select the server you want to add the bot to and authorize
89
+
90
+ To get your channel ID:
91
+ 1. In Discord, enable Developer Mode (User Settings > Advanced > Developer Mode)
92
+ 2. Right-click on the channel you want to send messages to
93
+ 3. Select "Copy ID"
94
+
95
+ ### Discord Configuration
96
+
97
+ Use the following code to initialize the Discord notifier:
98
+
99
+ ```python
100
+ from notte_core.notifiers.discord import DiscordConfig, DiscordNotifier
101
+
102
+ discord_config = DiscordConfig(
103
+ token="your-discord-bot-token-here",
104
+ channel_id=123456789012345678 # This should be an integer, not a string
105
+ )
106
+
107
+ discord_notifier = DiscordNotifier(discord_config)
108
+ ```
109
+
110
+ ## Using the Notifiers
111
+
112
+ Once configured, you can use the notifiers to send notifications:
113
+
114
+ ```python
115
+ from notte_core.common.agent.types import AgentResponse
116
+
117
+ # Create a sample response
118
+ response = AgentResponse(answer="This is a test notification")
119
+
120
+ # Send notifications
121
+ await slack_notifier.notify("Test Task", response)
122
+ await discord_notifier.notify("Test Task", response)
123
+ ```
124
+
125
+ The notifications will be formatted according to each platform's markdown styles:
126
+ - Slack uses `*text*` for bold
127
+ - Discord uses `**text**` for bold
128
+
129
+ Remember that both notifiers require async functions, so they must be called within an async context.
@@ -0,0 +1,41 @@
1
+ from typing import Any
2
+
3
+ import discord
4
+ from notte_agent.common.notifier import BaseNotifier
5
+ from pydantic import BaseModel
6
+ from typing_extensions import override
7
+
8
+
9
+ class DiscordConfig(BaseModel):
10
+ """Configuration for Discord sending functionality."""
11
+
12
+ token: str
13
+ channel_id: int
14
+
15
+
16
+ class DiscordNotifier(BaseNotifier):
17
+ """Discord notification implementation."""
18
+
19
+ def __init__(self, config: DiscordConfig) -> None:
20
+ super().__init__()
21
+ self.config: DiscordConfig = config
22
+ self._client: discord.Client = discord.Client(intents=discord.Intents.default())
23
+
24
+ @override
25
+ async def send_message(self, text: str) -> None:
26
+ """Send a message to the configured Discord channel."""
27
+ try:
28
+
29
+ @self._client.event
30
+ async def on_ready(): # pyright: ignore[reportUnusedFunction]
31
+ try:
32
+ channel = self._client.get_channel(self.config.channel_id)
33
+ if channel is None:
34
+ raise ValueError(f"Could not find channel with ID: {self.config.channel_id}")
35
+ _: Any = await channel.send(text) # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue]
36
+ finally:
37
+ await self._client.close()
38
+
39
+ await self._client.start(self.config.token)
40
+ except Exception as e:
41
+ raise ValueError(f"Failed to send Discord message: {str(e)}")
@@ -0,0 +1,67 @@
1
+ import smtplib
2
+ from email.mime.multipart import MIMEMultipart
3
+ from email.mime.text import MIMEText
4
+
5
+ from notte_agent.common.notifier import BaseNotifier
6
+ from pydantic import BaseModel
7
+ from typing_extensions import override
8
+
9
+
10
+ class EmailConfig(BaseModel):
11
+ """Configuration for email sending functionality."""
12
+
13
+ smtp_server: str
14
+ smtp_port: int = 587
15
+ sender_email: str
16
+ sender_password: str
17
+ receiver_email: str
18
+ subject: str = "Notte Agent Task Report"
19
+
20
+
21
+ class EmailNotifier(BaseNotifier):
22
+ """Email notification implementation."""
23
+
24
+ def __init__(self, config: EmailConfig) -> None:
25
+ super().__init__() # Call parent class constructor
26
+ self.config: EmailConfig = config
27
+ self._server: smtplib.SMTP | None = None
28
+
29
+ async def connect(self) -> None:
30
+ """Connect to the SMTP server."""
31
+ if self._server is not None:
32
+ return
33
+
34
+ self._server = smtplib.SMTP(host=self.config.smtp_server, port=self.config.smtp_port)
35
+ _ = self._server.starttls()
36
+ _ = self._server.login(user=self.config.sender_email, password=self.config.sender_password)
37
+
38
+ async def disconnect(self) -> None:
39
+ """Disconnect from the SMTP server."""
40
+ if self._server is not None:
41
+ _ = self._server.quit()
42
+ self._server = None
43
+
44
+ @override
45
+ async def send_message(self, text: str) -> None:
46
+ """Send an email with the given subject and body."""
47
+ await self.connect()
48
+ try:
49
+ if self._server is None:
50
+ await self.connect()
51
+
52
+ msg = MIMEMultipart()
53
+ msg["From"] = self.config.sender_email
54
+ msg["To"] = self.config.receiver_email
55
+ msg["Subject"] = self.config.subject
56
+
57
+ msg.attach(MIMEText(text, "plain"))
58
+
59
+ if self._server:
60
+ _ = self._server.send_message(msg)
61
+ finally:
62
+ await self.disconnect()
63
+
64
+ def __del__(self):
65
+ """Ensure SMTP connection is closed on deletion."""
66
+ if self._server is not None:
67
+ _ = self._server.quit()
@@ -0,0 +1,25 @@
1
+ from notte_agent.common.notifier import BaseNotifier
2
+ from pydantic import BaseModel
3
+ from slack_sdk.web.client import WebClient
4
+ from typing_extensions import override
5
+
6
+
7
+ class SlackConfig(BaseModel):
8
+ """Configuration for Slack sending functionality."""
9
+
10
+ token: str
11
+ channel_id: str
12
+
13
+
14
+ class SlackNotifier(BaseNotifier):
15
+ """Slack notification implementation."""
16
+
17
+ def __init__(self, config: SlackConfig) -> None:
18
+ super().__init__()
19
+ self.config: SlackConfig = config
20
+ self._client: WebClient = WebClient(token=self.config.token)
21
+
22
+ @override
23
+ async def send_message(self, text: str) -> None:
24
+ """Send a message to the configured Slack channel."""
25
+ _ = self._client.chat_postMessage(channel=self.config.channel_id, text=text) # type: ignore[type_unknown]
@@ -0,0 +1,57 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ import requests
5
+ from loguru import logger
6
+ from pydantic import Field
7
+ from typing_extensions import override
8
+
9
+ from notte_integrations.sessions.cdp_session import CDPSession, CDPSessionsManager
10
+
11
+
12
+ def get_anchor_api_key() -> str:
13
+ anchor_api_key: str | None = os.getenv("ANCHOR_API_KEY")
14
+ if anchor_api_key is None:
15
+ raise ValueError("ANCHOR_API_KEY is not set")
16
+ return anchor_api_key
17
+
18
+
19
+ class AnchorSessionsManager(CDPSessionsManager):
20
+ anchor_base_url: str = "https://api.anchorbrowser.io"
21
+ use_proxy: bool = True
22
+ solve_captcha: bool = True
23
+ anchor_api_key: str = Field(default_factory=get_anchor_api_key)
24
+
25
+ @override
26
+ def create_session_cdp(self) -> CDPSession:
27
+ if self.verbose:
28
+ logger.info("Creating Anchor session...")
29
+
30
+ browser_configuration: dict[str, Any] = {}
31
+
32
+ if self.use_proxy:
33
+ browser_configuration["proxy_config"] = {"type": "anchor-residential", "active": True}
34
+
35
+ if self.solve_captcha:
36
+ browser_configuration["captcha_config"] = {"active": True}
37
+
38
+ response = requests.post(
39
+ f"{self.anchor_base_url}/api/sessions",
40
+ headers={
41
+ "anchor-api-key": self.anchor_api_key,
42
+ "Content-Type": "application/json",
43
+ },
44
+ json=browser_configuration,
45
+ )
46
+ response.raise_for_status()
47
+ session_id: str = response.json()["id"]
48
+ return CDPSession(
49
+ session_id=session_id,
50
+ cdp_url=f"wss://connect.anchorbrowser.io?apiKey={self.anchor_api_key}&sessionId={session_id}",
51
+ )
52
+
53
+ @override
54
+ def close_session_cdp(self, session_id: str) -> bool:
55
+ if self.verbose:
56
+ logger.info(f"Closing CDP session for URL {session_id}")
57
+ return True
@@ -0,0 +1,82 @@
1
+ import os
2
+
3
+ from loguru import logger
4
+ from typing_extensions import override
5
+
6
+ from notte_integrations.sessions.cdp_session import CDPSession, CDPSessionsManager
7
+
8
+ try:
9
+ from browserbase import Browserbase
10
+ except ImportError:
11
+ raise ImportError("Install with notte[browserbase] to include browserbase integration")
12
+
13
+
14
+ # TODO: use api with requests instead of sdk so we don't have the added dependency
15
+ class BrowserBaseSessionsManager(CDPSessionsManager):
16
+ def __init__(
17
+ self,
18
+ verbose: bool = False,
19
+ stealth: bool = True,
20
+ ):
21
+ super().__init__()
22
+
23
+ bb_api_key: str | None = os.getenv("BROWSERBASE_API_KEY")
24
+ bb_project_id: str | None = os.getenv("BROWSERBASE_PROJECT_ID")
25
+
26
+ if bb_api_key is None:
27
+ raise ValueError("BROWSERBASE_API_KEY env variable is not set")
28
+
29
+ if bb_project_id is None:
30
+ raise ValueError("BROWSERBASE_PROJECT_ID env variable is not set")
31
+
32
+ self.bb_api_key: str = bb_api_key
33
+ self.bb_project_id: str = bb_project_id
34
+
35
+ self.bb: Browserbase = Browserbase(api_key=self.bb_api_key)
36
+ self.stealth: bool = stealth
37
+ self.verbose: bool = verbose
38
+
39
+ @override
40
+ def create_session_cdp(self) -> CDPSession:
41
+ if self.verbose:
42
+ logger.info("Creating BrowserBase session...")
43
+
44
+ stealth_args = dict(
45
+ browser_settings={
46
+ "fingerprint": {
47
+ "browsers": ["chrome", "firefox", "edge", "safari"],
48
+ "devices": ["mobile", "desktop"],
49
+ "locales": ["en-US", "en-GB"],
50
+ "operatingSystems": ["android", "ios", "linux", "macos", "windows"],
51
+ "screen": {
52
+ "maxHeight": 1080,
53
+ "maxWidth": 1920,
54
+ "minHeight": 1080,
55
+ "minWidth": 1920,
56
+ },
57
+ "viewport": {
58
+ "width": 1920,
59
+ "height": 1080,
60
+ },
61
+ },
62
+ "solveCaptchas": True,
63
+ },
64
+ proxies=True,
65
+ )
66
+
67
+ args = stealth_args if self.stealth else {}
68
+ session = self.bb.sessions.create(project_id=self.bb_project_id, **args) # type: ignore
69
+
70
+ if self.verbose:
71
+ logger.info(f"Got BrowserBase session {session}")
72
+
73
+ return CDPSession(
74
+ session_id=session.id,
75
+ cdp_url=session.connect_url,
76
+ )
77
+
78
+ @override
79
+ def close_session_cdp(self, session_id: str) -> bool:
80
+ if self.verbose:
81
+ logger.info(f"Closing CDP session for URL {session_id}")
82
+ return True
@@ -0,0 +1,77 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from loguru import logger
4
+ from notte_browser.playwright import WindowManager
5
+ from notte_browser.window import BrowserResource, BrowserWindowOptions
6
+ from notte_sdk.types import BrowserType
7
+ from patchright.async_api import Browser as PatchrightBrowser
8
+ from pydantic import BaseModel, Field
9
+ from typing_extensions import override
10
+
11
+
12
+ class CDPSession(BaseModel):
13
+ session_id: str
14
+ cdp_url: str
15
+ resource: BrowserResource | None = None
16
+
17
+
18
+ class CDPSessionsManager(WindowManager, ABC):
19
+ sessions: dict[str, CDPSession] = Field(default_factory=dict)
20
+ last_session: CDPSession | None = Field(default=None)
21
+ browser_type: BrowserType = Field(default=BrowserType.CHROMIUM)
22
+
23
+ @abstractmethod
24
+ def create_session_cdp(self) -> CDPSession:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def close_session_cdp(self, session_id: str) -> bool:
29
+ pass
30
+
31
+ @override
32
+ async def create_playwright_browser(self, options: BrowserWindowOptions) -> PatchrightBrowser:
33
+ session = self.create_session_cdp()
34
+ if session.cdp_url in self.sessions:
35
+ raise ValueError(f"Session {session.session_id} already exists")
36
+
37
+ cdp_options = options.set_cdp_url(session.cdp_url)
38
+ logger.info(f"Connecting to CDP at {cdp_options.cdp_url}")
39
+ browser = await self.connect_cdp_browser(cdp_options)
40
+ self.sessions[session.cdp_url] = session
41
+ self.last_session = session
42
+ return browser
43
+
44
+ @override
45
+ async def get_browser_resource(self, options: BrowserWindowOptions) -> BrowserResource:
46
+ resource = await super().get_browser_resource(options)
47
+ cdp_url = resource.options.cdp_url
48
+ if cdp_url is None:
49
+ if self.last_session is None:
50
+ raise ValueError(f"CDP URL is not set for resource {cdp_url} and last session is not set")
51
+ logger.info(f"Setting CDP URL for resource {cdp_url} to {self.last_session.cdp_url}")
52
+ resource.options = resource.options.set_cdp_url(self.last_session.cdp_url)
53
+ cdp_url = self.last_session.cdp_url
54
+ if cdp_url not in self.sessions:
55
+ raise ValueError(f"Session {cdp_url} not found")
56
+ self.sessions[cdp_url].resource = resource
57
+ self.browser = None # pyright: ignore[reportUnannotatedClassAttribute]
58
+ self.last_session = None
59
+ return resource
60
+
61
+ @override
62
+ async def release_browser_resource(self, resource: BrowserResource) -> None:
63
+ await super().release_browser_resource(resource)
64
+ cdp_url = resource.options.cdp_url
65
+ if cdp_url not in self.sessions:
66
+ raise ValueError(f"Session {cdp_url} not found")
67
+ session = self.sessions[cdp_url]
68
+ status = self.close_session_cdp(session.session_id)
69
+ if not status:
70
+ logger.error(f"Failed to close session {session.session_id}")
71
+ del self.sessions[cdp_url]
72
+
73
+ @override
74
+ async def stop(self) -> None:
75
+ await super().stop()
76
+ for session in self.sessions.values():
77
+ _ = self.close_session_cdp(session.session_id)
@@ -0,0 +1,53 @@
1
+ import os
2
+
3
+ import requests
4
+ from loguru import logger
5
+ from pydantic import Field
6
+ from typing_extensions import override
7
+
8
+ from notte_integrations.sessions.cdp_session import CDPSession, CDPSessionsManager
9
+
10
+
11
+ def get_steel_api_key() -> str:
12
+ steel_api_key: str | None = os.getenv("STEEL_API_KEY")
13
+ if steel_api_key is None:
14
+ raise ValueError("STEEL_API_KEY is not set")
15
+ return steel_api_key
16
+
17
+
18
+ class SteelSessionsManager(CDPSessionsManager):
19
+ steel_base_url: str = "api.steel.dev" # localhost:3000"
20
+ steel_api_key: str = Field(default_factory=get_steel_api_key)
21
+
22
+ @override
23
+ def create_session_cdp(self) -> CDPSession:
24
+ logger.info("Creating Steel session...")
25
+
26
+ url = f"https://{self.steel_base_url}/v1/sessions"
27
+
28
+ headers = {"Steel-Api-Key": self.steel_api_key}
29
+
30
+ response = requests.post(url, headers=headers)
31
+ response.raise_for_status()
32
+ data: dict[str, str] = response.json()
33
+ if "localhost" in self.steel_base_url:
34
+ cdp_url = f"ws://{self.steel_base_url}/v1/devtools/browser/{data['id']}"
35
+ else:
36
+ cdp_url = f"wss://connect.steel.dev?apiKey={self.steel_api_key}&sessionId={data['id']}"
37
+ return CDPSession(session_id=data["id"], cdp_url=cdp_url)
38
+
39
+ @override
40
+ def close_session_cdp(self, session_id: str) -> bool:
41
+ if self.verbose:
42
+ logger.info(f"Closing CDP session for URL {session_id}")
43
+
44
+ url = f"https://{self.steel_base_url}/v1/sessions/{session_id}/release"
45
+
46
+ headers = {"Steel-Api-Key": self.steel_api_key}
47
+
48
+ response = requests.post(url, headers=headers)
49
+ if response.status_code != 200:
50
+ if self.verbose:
51
+ logger.error(f"Failed to release Steel session {session_id}: {response.json()}")
52
+ return False
53
+ return True