diff --git a/.env.example b/.env.example
index c94d292..20f8357 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,7 @@
+<<<<<<< HEAD
+=======
# 권장 (다른 모델로 교체 가능) [다른 모델로 교체시 성능 보장 불가]
+>>>>>>> main
ANONYMIZED_TELEMETRY=false
# ========== LLM ==========
@@ -22,27 +25,17 @@ PROXY_HOST=127.0.0.1
PROXY_PORT=11080
BACKEND_URL=http://localhost:11081
-# https://docs.browser-use.com/development/observability
+# https://docs.browser-use.com/development/observability - 선택
# Lmnr 계정이 필요합니다.
# https://lmnr.ai/
LMNR_PROJECT_API_KEY=
# 브라우저 언어 설정
LANG=en_US
+HEADLESS=False # 브라우저를 헤드리스 모드로 실행할지 여부. True로 설정하면 브라우저가 보이지 않습니다.
# ========= Account ==========
+# 필수 뒤에 있는 이메일 주소는 Google 계정의 로그인 힌트로 사용됩니다.
+# 이메일의 전체를 입력해주세요
GOOGLE_ID=whs.imnya.ng@gmail.com
-GOOGLE_PASSWORD=Vb1Mz9pgjY8JVs
-
-NAVER_ID=oauth-j93es
-NAVER_PASSWORD=whs31234
-
-FACEBOOK_ID=01047183675
-FACEBOOK_PASSWORD=whs3oauth@
-
-GITGUB_ID=imnyang-bot
-GITHUB_PASSWORD=6PuVXCH9tpQLNm
-
-MICROSOFT_ID=whs.imnya.ng@gmail.com
-MICROSOFT_PASSWORD=WHS123987
\ No newline at end of file
diff --git a/.github/instructions/agent-settings.instructions.md b/.github/instructions/agent-settings.instructions.md
deleted file mode 100644
index 60b58b3..0000000
--- a/.github/instructions/agent-settings.instructions.md
+++ /dev/null
@@ -1,345 +0,0 @@
----
-description: "Learn how to configure the agent"
-applyTo: '**'
----
-
-## Overview
-
-The `Agent` class is the core component of Browser Use that handles browser automation. Here are the main configuration options you can use when initializing an agent.
-
-## Basic Settings
-
-```python
-from browser_use import Agent
-from langchain_openai import ChatOpenAI
-
-agent = Agent(
- task="Search for latest news about AI",
- llm=ChatOpenAI(model="gpt-4o"),
-)
-```
-
-### Required Parameters
-
-- `task`: The instruction for the agent to execute
-- `llm`: A LangChain chat model instance. See LangChain Models for supported models.
-
-## Agent Behavior
-
-Control how the agent operates:
-
-```python
-agent = Agent(
- task="your task",
- llm=llm,
- controller=custom_controller, # For custom tool calling
- use_vision=True, # Enable vision capabilities
- save_conversation_path="logs/conversation" # Save chat logs
-)
-```
-
-### Behavior Parameters
-
-- `controller`: Registry of functions the agent can call. Defaults to base Controller. See Custom Functions for details.
-- `use_vision`: Enable/disable vision capabilities. Defaults to `True`.
- - When enabled, the model processes visual information from web pages
- - Disable to reduce costs or use models without vision support
- - For GPT-4o, image processing costs approximately 800-1000 tokens (~$0.002 USD) per image (but this depends on the defined screen size)
-- `save_conversation_path`: Path to save the complete conversation history. Useful for debugging.
-- `override_system_message`: Completely replace the default system prompt with a custom one.
-- `extend_system_message`: Add additional instructions to the default system prompt.
-
-
- Vision capabilities are recommended for better web interaction understanding,
- but can be disabled to reduce costs or when using models without vision
- support.
-
-
-
-### Reuse Existing Browser Context
-
-By default browser-use launches its own builtin browser using playwright chromium.
-You can also connect to a remote browser or pass any of the following
-existing playwright objects to the Agent: `page`, `browser_context`, `browser`, `browser_session`, or `browser_profile`.
-
-These all get passed down to create a `BrowserSession` for the `Agent`:
-
-
-```python
-agent = Agent(
- task='book a flight to fiji',
- llm=llm,
- browser_profile=browser_profile, # use this profile to create a BrowserSession
- browser_session=BrowserSession( # use an existing BrowserSession
- cdp_url=..., # remote CDP browser to connect to
- # or
- wss_url=..., # remote wss playwright server provider
- # or
- browser_pid=... # pid of a locally running browser process to attach to
- # or
- executable_path=... # provide a custom chrome binary path
- # or
- channel=... # specify chrome, chromium, ms-edge, etc.
- # or
- page=page, # use an existing playwright Page object
- # or
- browser_context=browser_context, # use an existing playwright BrowserContext object
- # or
- browser=browser, # use an existing playwright Browser object
- ),
-)
-```
-
-For example, to connect to an existing browser over CDP you could do:
-```python
-agent = Agent(
- ...
- browser_session=BrowserSession(cdp_url='http://localhost:9222'),
-)
-```
-
-For example, to connect to a local running chrome instance you can do:
-```python
-agent = Agent(
- ...
- browser_session=BrowserSession(browser_pid=1234),
-)
-```
-
-See Connect to your Browser for more info.
-
-
- You can reuse the same `BrowserSession` after an agent has completed running. If you do nothing, the
- browser will be automatically closed on `run()` completion only if it was launched by us.
-
-
-## Running the Agent
-
-The agent is executed using the async `run()` method:
-
-- `max_steps` (default: `100`)
- Maximum number of steps the agent can take during execution. This prevents infinite loops and helps control execution time.
-
-## Agent History
-
-The method returns an `AgentHistoryList` object containing the complete execution history. This history is invaluable for debugging, analysis, and creating reproducible scripts.
-
-```python
-# Example of accessing history
-history = await agent.run()
-
-# Access (some) useful information
-history.urls() # List of visited URLs
-history.screenshots() # List of screenshot paths
-history.action_names() # Names of executed actions
-history.extracted_content() # Content extracted during execution
-history.errors() # Any errors that occurred
-history.model_actions() # All actions with their parameters
-```
-
-The `AgentHistoryList` provides many helper methods to analyze the execution:
-
-- `final_result()`: Get the final extracted content
-- `is_done()`: Check if the agent completed successfully
-- `has_errors()`: Check if any errors occurred
-- `model_thoughts()`: Get the agent's reasoning process
-- `action_results()`: Get results of all actions
-
-
- For a complete list of helper methods and detailed history analysis
- capabilities, refer to the [AgentHistoryList source
- code](https://github.com/browser-use/browser-use/blob/main/browser_use/agent/views.py#L111).
-
-
-## Run initial actions without LLM
-With [this example](https://github.com/browser-use/browser-use/blob/main/examples/features/initial_actions.py) you can run initial actions without the LLM.
-Specify the action as a dictionary where the key is the action name and the value is the action parameters. You can find all our actions in the [Controller](https://github.com/browser-use/browser-use/blob/main/browser_use/controller/service.py) source code.
-```python
-
-initial_actions = [
- {'open_tab': {'url': 'https://www.google.com'}},
- {'open_tab': {'url': 'https://en.wikipedia.org/wiki/Randomness'}},
- {'scroll_down': {'amount': 1000}},
-]
-agent = Agent(
- task='What theories are displayed on the page?',
- initial_actions=initial_actions,
- llm=llm,
-)
-```
-
-## Run with message context
-
-You can configure the agent and provide a separate message to help the LLM understand the task better.
-
-```python
-from langchain_openai import ChatOpenAI
-
-agent = Agent(
- task="your task",
- message_context="Additional information about the task",
- llm = ChatOpenAI(model='gpt-4o')
-)
-```
-
-## Run with planner model
-
-You can configure the agent to use a separate planner model for high-level task planning:
-
-```python
-from langchain_openai import ChatOpenAI
-
-# Initialize models
-llm = ChatOpenAI(model='gpt-4o')
-planner_llm = ChatOpenAI(model='o3-mini')
-
-agent = Agent(
- task="your task",
- llm=llm,
- planner_llm=planner_llm, # Separate model for planning
- use_vision_for_planner=False, # Disable vision for planner
- planner_interval=4 # Plan every 4 steps
-)
-```
-
-### Planner Parameters
-
-- `planner_llm`: A LangChain chat model instance used for high-level task planning. Can be a smaller/cheaper model than the main LLM.
-- `use_vision_for_planner`: Enable/disable vision capabilities for the planner model. Defaults to `True`.
-- `planner_interval`: Number of steps between planning phases. Defaults to `1`.
-
-Using a separate planner model can help:
-- Reduce costs by using a smaller model for high-level planning
-- Improve task decomposition and strategic thinking
-- Better handle complex, multi-step tasks
-
-
- The planner model is optional. If not specified, the agent will not use the planner model.
-
-
-### Optional Parameters
-
-- `message_context`: Additional information about the task to help the LLM understand the task better.
-- `initial_actions`: List of initial actions to run before the main task.
-- `max_actions_per_step`: Maximum number of actions to run in a step. Defaults to `10`.
-- `max_failures`: Maximum number of failures before giving up. Defaults to `3`.
-- `retry_delay`: Time to wait between retries in seconds when rate limited. Defaults to `10`.
-- `generate_gif`: Enable/disable GIF generation. Defaults to `False`. Set to `True` or a string path to save the GIF.
-## Memory Management
-
-Browser Use includes a procedural memory system using [Mem0](https://mem0.ai) that automatically summarizes the agent's conversation history at regular intervals to optimize context window usage during long tasks.
-
-```python
-from browser_use.agent.memory import MemoryConfig
-
-agent = Agent(
- task="your task",
- llm=llm,
- enable_memory=True,
- memory_config=MemoryConfig( # Ensure llm_instance is passed if not using default LLM config
- llm_instance=llm, # Important: Pass the agent's LLM instance here
- agent_id="my_custom_agent",
- memory_interval=15
- )
-)
-```
-
-### Memory Parameters
-
-- `enable_memory`: Enable/disable the procedural memory system. Defaults to `True`.
-- `memory_config`: A `MemoryConfig` Pydantic model instance (required if `enable_memory` is `True`). Dictionary format is not supported.
-
-### Using MemoryConfig
-
-You must configure the memory system using the `MemoryConfig` Pydantic model for a type-safe approach:
-
-```python
-from browser_use.agent.memory import MemoryConfig
-from langchain_openai import ChatOpenAI # Assuming llm is an instance of ChatOpenAI
-
-llm_for_agent = ChatOpenAI(model="gpt-4o")
-
-agent = Agent(
- task=task_description,
- llm=llm_for_agent,
- enable_memory=True, # This is True by default
- memory_config=MemoryConfig(
- llm_instance=llm_for_agent, # Pass the LLM instance for Mem0
- agent_id="my_agent",
- memory_interval=15, # Summarize every 15 steps
- embedder_provider="openai",
- embedder_model="text-embedding-3-large",
- embedder_dims=1536,
- # --- Vector Store Customization ---
- vector_store_provider="qdrant", # e.g., Qdrant, Pinecone, Chroma, etc.
- vector_store_collection_name="my_browser_use_memories", # Optional: custom collection name
- vector_store_config_override={ # Provider-specific config
- "host": "localhost",
- "port": 6333
- # Add other Qdrant specific configs here if needed, e.g., api_key for cloud
- }
- )
-)
-```
-
-The `MemoryConfig` model provides these configuration options:
-
-#### Memory Settings
-- `agent_id`: Unique identifier for the agent (default: `"browser_use_agent"`). Essential for persistent memory sessions if using a persistent vector store.
-- `memory_interval`: Number of steps between memory summarization (default: `10`)
-
-#### LLM Settings (for Mem0's internal operations)
-- `llm_instance`: The LangChain `BaseChatModel` instance that Mem0 will use for its internal summarization and processing. You must pass the same LLM instance used by the main agent, or another compatible one, here.
-
-#### Embedder Settings
-- `embedder_provider`: Provider for embeddings (`'openai'`, `'gemini'`, `'ollama'`, or `'huggingface'`)
-- `embedder_model`: Model name for the embedder
-- `embedder_dims`: Dimensions for the embeddings
-
-#### Vector Store Settings
-- `vector_store_provider`: Choose the vector store backend. Supported options include:
- `'faiss'` (default), `'qdrant'`, `'pinecone'`, `'supabase'`, `'elasticsearch'`, `'chroma'`, `'weaviate'`, `'milvus'`, `'pgvector'`, `'upstash_vector'`, `'vertex_ai_vector_search'`, `'azure_ai_search'`, `'lancedb'`, `'mongodb'`, `'redis'`, `'memory'` (in-memory, non-persistent).
-- `vector_store_collection_name`: (Optional) Specify a custom name for the collection or index in your vector store. If not provided, a default name is generated (especially for local stores like FAISS/Chroma) or used by Mem0.
-- `vector_store_base_path`: Path for local vector stores like FAISS or Chroma (e.g., `/tmp/mem0`). Default is `/tmp/mem0`.
-- `vector_store_config_override`: (Optional) A dictionary to provide or override specific configuration parameters required by Mem0 for the chosen `vector_store_provider`. This is where you'd put connection details like `host`, `port`, `api_key`, `url`, `environment`, etc., for cloud-based or server-based vector stores.
-
-The model automatically sets appropriate defaults based on the LLM being used:
-- For `ChatOpenAI`: Uses OpenAI's `text-embedding-3-small` embeddings
-- For `ChatGoogleGenerativeAI`: Uses Gemini's `models/text-embedding-004` embeddings
-- For `ChatOllama`: Uses Ollama's `nomic-embed-text` embeddings
-- Default: Uses Hugging Face's `all-MiniLM-L6-v2` embeddings
-
-
- **Important:**
- - Always pass a properly constructed `MemoryConfig` object to the `memory_config` parameter.
- - Ensure the `llm_instance` is provided to `MemoryConfig` so Mem0 can perform its operations.
- - For persistent memory across agent runs or for shared memory, choose a scalable vector store provider (like Qdrant, Pinecone, etc.) and configure it correctly using `vector_store_provider` and `vector_store_config_override`. The default 'faiss' provider stores data locally in `vector_store_base_path`.
-
-
-### How Memory Works
-
-When enabled, the agent periodically compresses its conversation history into concise summaries:
-
-1. Every `memory_interval` steps, the agent reviews its recent interactions.
-2. It uses Mem0 (configured with your chosen LLM and vector store) to create a procedural memory summary.
-3. The original messages in the agent's active context are replaced with this summary, reducing token usage.
-4. This process helps maintain important context while freeing up the context window for new information.
-
-
-### Disabling Memory
-
-If you want to disable the memory system (for debugging or for shorter tasks), set `enable_memory` to `False`:
-
-```python
-agent = Agent(
- task="your task",
- llm=llm,
- enable_memory=False
-)
-```
-
-
- Disabling memory may be useful for debugging or short tasks, but for longer
- tasks, it can lead to context window overflow as the conversation history
- grows. The memory system helps maintain performance during extended sessions.
-
diff --git a/.github/instructions/browser-settings.instructions.md b/.github/instructions/browser-settings.instructions.md
deleted file mode 100644
index 4e201ea..0000000
--- a/.github/instructions/browser-settings.instructions.md
+++ /dev/null
@@ -1,968 +0,0 @@
----
-description: "Launch or connect to an existing browser and configure it to your needs."
-applyTo: '**'
----
-
-Browser Use uses [playwright](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context) (or [patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright)) to manage its connection with a real browser.
-
----
-
-**To launch or connect to a browser**, pass any playwright / browser-use configuration arguments you want to `BrowserSession(...)`:
-
-```python
-from browser_use import BrowserSession, Agent
-
-browser_session = BrowserSession(
- headless=True,
- viewport={'width': 964, 'height': 647},
- user_data_dir='~/.config/browseruse/profiles/default',
-)
-agent = Agent('fill out the form on this page', browser_session=browser_session)
-```
-
-
- The new `BrowserSession` & `BrowserProfile` accept all the same arguments that Playwright's [`launch_persistent_context(...)`](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context) takes, giving you full control over browser settings at launch. (see below for the full list)
-
-
-
----
-
-## `BrowserSession`
-
-- 🎭 `BrowserSession(**params)` is Browser Use's object that tracks a playwright connection to a running browser. It sets up:
- - the `playwright` library, `browser` and/or `browser_context`, and `page` objects and tracks which tabs the agent & human are focused on
- - methods to interact with the browser window, apply config needed by the Agent, and run the `DOMService` for element detection
- - it can take a `browser_profile=BrowserProfile(...)` template containing some config defaults, and `**kwargs` session-specific config overrides
-
-### Browser Connection Parameters
-
-Provide any one of these options to connect to an existing browser. These options are session-specific and cannot be stored in a `BrowserProfile(...)` template.
-
-#### `wss_url`
-
-```python
-wss_url: str | None = None
-```
-
-WSS URL of the playwright-protocol browser server to connect to. See here for [WSS connection instructions](https://docs.browser-use.com/customize/real-browser#method-d%3A-connect-to-remote-playwright-node-js-browser-server-via-wss-url).
-
-#### `cdp_url`
-
-```python
-cdp_url: str | None = None
-```
-
-CDP URL of the browser to connect to (e.g. `http://localhost:9222`). See here for [CDP connection instructions](https://docs.browser-use.com/customize/real-browser#method-e%3A-connect-to-remote-browser-via-cdp-url).
-
-#### `browser_pid`
-
-```python
-browser_pid: int | None = None
-```
-
-PID of a running chromium-based browser process to connect to on localhost. See here for [connection via pid](https://docs.browser-use.com/customize/real-browser#method-c%3A-connect-to-local-browser-using-browser-pid) instructions.
-
-
- For web scraping tasks on sites that restrict automated access, we recommend
- using [our cloud](https://browser-use.com) or an external browser provider for better reliability.
- See the [Connect to your Browser](real-browser) guide for detailed connection instructions.
-
-
-### Session-Specific Parameters
-
-#### `browser_profile`
-
-```python
-browser_profile: BrowserProfile = BrowserProfile()
-```
-
-Optional `BrowserProfile` template containing default config to use for the `BrowserSession`. (see below for more info)
-
-#### `playwright`
-
-```python
-playwright: Playwright | None = None
-```
-
-Optional playwright or patchright API client handle to use, the result of `(await async_playwright().start())` or `(await async_patchright().start())`, which spawns a node.js child subprocess that relays commands to the browser over CDP.
-
-See here for [more detailed usage instructions](https://docs.browser-use.com/customize/real-browser#method-b%3A-connect-using-existing-playwright-objects).
-
-#### `browser`
-
-```python
-browser: Browser | None = None
-```
-
-Playwright Browser object to use (optional). See here for [more detailed usage instructions](https://docs.browser-use.com/customize/real-browser#method-b%3A-connect-using-existing-playwright-objects).
-
-#### `browser_context`
-
-```python
-browser_context: BrowserContext | None = None
-```
-
-Playwright BrowserContext object to use (optional). See here for [more detailed usage instructions](https://docs.browser-use.com/customize/real-browser#method-b%3A-connect-using-existing-playwright-objects).
-
-#### `page` *aka* `agent_current_page`
-
-
-
-```python
-page: Page | None = None
-```
-
-Foreground Page that the agent is focused on, can also be passed as `page=...` as a shortcut. See here for [more detailed usage instructions](https://docs.browser-use.com/customize/real-browser#method-b%3A-connect-using-existing-playwright-objects).
-
-#### `human_current_page`
-
-```python
-human_current_page: Page | None = None
-```
-
-Foreground Page that the human is focused on to start, not necessary to set manually.
-
-#### `initialized`
-
-```python
-initialized: bool = False
-```
-
-Mark BrowserSession as already initialized, skips launch/connection (not recommended)
-
-
-#### `**kwargs`
-
-`BrowserSession` can also accept *all* of the parameters [below](#browserprofile).
-(the parameters *above* this point are specific to `BrowserSession` and cannot be stored in a `BrowserProfile` template)
-
-Extra `**kwargs` passed to `BrowserSession(...)` act as session-specific overrides to the `BrowserProfile(...)` template.
-
-```python
-base_iphone13 = BrowserProfile(
- storage_state='/tmp/auth.json', # share cookies between parallel browsers
- **playwright.devices['iPhone 13'],
- timezone_id='UTC',
-)
-usa_phone = BrowserSession(
- browser_profile=base_iphone13,
- timezone_id='America/New_York', # kwargs override values in base_iphone13
-)
-eu_phone = BrowserSession(
- browser_profile=base_iphone13,
- timezone_id='Europe/Paris',
-)
-
-usa_agent = Agent(task='show me todays schedule...', browser_session=usa_phone)
-eu_agent = Agent(task='show me todays schedule...', browser_session=eu_phone)
-await asyncio.gather(agent1.run(), agent2.run())
-```
-
----
-
-
-## `BrowserProfile`
-
-A `BrowserProfile` is a 📋 config template for a 🎭 `BrowserSession(...)`.
-
-It's basically just a typed + validated version of a `dict` to hold config.
-
-When you find yourself storing or re-using many browser configs, you can upgrade from:
-
-```diff
-- config = {key: val, key: val, ...}
-- BrowserSession(**config)
-```
-To this instead:
-```diff
-+ config = BrowserProfile(key=val, key=val, ...)
-+ BrowserSession(browser_profile=config)
-```
-
-
-You don't ever *need* to use a `BrowserProfile`, you can always pass config parameters directly to `BrowserSession`:
-```python
-session = BrowserSession(headless=True, storage_state='auth.json', viewport={...}, ...)
-```
-
-
-`BrowserProfile` is optional, but it provides a number of benefits over a normal `dict` for holding config:
-
-- has type hints and pydantic field descriptions that show up in your IDE
-- validates config at runtime quickly without having to start a browser
-- provides helper methods to autodetect screen size, set up local paths, save/load config as json, and more...
-
-
-`BrowserProfiles`s are designed to easily be given 🆔 `uuid`s and put in a database + made editable by users.
-`BrowserSession`s get their own 🆔 `uuid`s and be linked by 🖇 foreign key to whatever `BrowserProfiles` they use.
-
-This cleanly separates the per-connection rows from the bulky re-usable config and avoids wasting space in your db.
-This is useful because a user may only have 2 or 3 profiles, but they could have 100k+ sessions within a few months.
-
-
-
-`BrowserProfile` and `BrowserSession` can both take any of the:
-
-- [Playwright parameters](#playwright)
-- [Browser-Use parameters](#browser-use-parameters) (extra options we provide on top of `playwright`)
-
-The only parameters `BrowserProfile` can NOT take are the session-specific connection parameters and live playwright objects:
-`cdp_url`, `wss_url`, `browser_pid`, `page`, `browser`, `browser_context`, `playwright`, etc.
-
-### Basic Example
-
-```python
-from browser_use.browser import BrowserProfile
-
-profile = BrowserProfile(
- stealth=True,
- storage_state='/tmp/google_docs_cookies.json',
- allowed_domains=['docs.google.com', 'https://accounts.google.com'],
- viewport={'width': 396, 'height': 774},
- # ... playwright args / browser-use config args ...
-)
-
-phone1 = BrowserSession(browser_profile=profile, device_scale_factor=1)
-phone2 = BrowserSession(browser_profile=profile, device_scale_factor=2)
-phone3 = BrowserSession(browser_profile=profile, device_scale_factor=3)
-```
-
-### Browser-Use Parameters
-
-These parameters control Browser Use-specific features, and are outside the standard playwright set. They can be passed to `BrowserSession(...)` and/or stored in a `BrowserProfile` template.
-
-#### `keep_alive`
-
-```python
-keep_alive: bool | None = None
-```
-
-If `True` it wont close the browser after the first `agent.run()` ends. Useful for running multiple tasks with the same browser instance. If this is left as `None` and the Agent launched its own browser, the default is to close the browser after the agent completes. If the agent connected to an existing browser then it will leave it open.
-
-#### `stealth`
-
-```python
-stealth: bool = False
-```
-Set to `True` to use [`patchright`](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) to avoid bot-blocking. (Might cause issues with some sites, requires manual testing.)
-
-
-
-#### `allowed_domains`
-
-```python
-allowed_domains: list[str] | None = None
-```
-
-List of allowed domains for navigation. If None, all domains are allowed.
-Example: `['google.com', '*.wikipedia.org']` - Here the agent will only be able to access `google.com` exactly and `wikipedia.org` + `*.wikipedia.org`.
-
-Glob patterns are supported:
-- `['example.com']` ✅ will match only `https://example.com/*` exactly, subdomains will not be allowed.
- It's always the most secure to list all the domains you want to give the access to explicitly w/ schemes e.g.
- `['https://google.com', 'http*://www.google.com', 'https://myaccount.google.com', 'https://mail.google.com', 'https://docs.google.com']`
-- `['*.example.com']` ⚠️ **CAUTION** this will match `https://example.com` and *all* its subdomains.
- Make sure *all* the subdomains are safe for the agent! `abc.example.com`, `def.example.com`, ..., `useruploads.example.com`, `admin.example.com`
-
-#### `disable_security`
-
-```python
-disable_security: bool = False
-```
-
-Completely disables all basic browser security features. Allows interacting across cross-site iFrames boundaries, but
-
-
-This option is very INSECURE and is only for niche use cases. DO NOT LET YOUR AGENT visit untrusted URLs or give it real cookies when `disable_security=True`.
-Visiting a single malicious site in this mode can trivially compromise *all* the cookies in the browser profile in under 1 second.
-
-
-#### `deterministic_rendering`
-
-```python
-deterministic_rendering: bool = False
-```
-
-Attempt to forced more deterministic rendering for consistent screenshots across different host operating systems and hardware.
-
-Disables OS-specific font hints, aliasing, GPU-accelerated rendering, normalizes DPI, and sets a specific JS random seed to try to avoid nondeterministic JS.
-
-
-This flag is for niche use cases (e.g. screenshot diffing) where pixel-perfect rendering across different server operating systems is more important than stability.
-It makes the agent more likely to be blocked as a bot and triggers some glitchy behavior in chrome occasionally, it's not recommended unless you know you need it.
-
-
-#### `highlight_elements`
-
-```python
-highlight_elements: bool = True
-```
-
-Highlight interactive elements on the screen with colorful bounding boxes.
-
-#### `viewport_expansion`
-
-```python
-viewport_expansion: int = 500
-```
-
-Viewport expansion in pixels. With this you can control how much of the page is included in the context of the LLM:
-- `-1`: All elements from the entire page will be included, regardless of visibility (highest token usage but most complete).
-- `0`: Only elements which are currently visible in the viewport will be included.
-- `500` (default): Elements in the viewport plus an additional 500 pixels in each direction will be included, providing a balance between context and token usage.
-
-#### `include_dynamic_attributes`
-
-```python
-include_dynamic_attributes: bool = True
-```
-
-Include dynamic attributes in selectors for better element targeting.
-
-#### `minimum_wait_page_load_time`
-
-```python
-minimum_wait_page_load_time: float = 0.25
-```
-
-Minimum time to wait before capturing page state for LLM input.
-
-#### `wait_for_network_idle_page_load_time`
-
-```python
-wait_for_network_idle_page_load_time: float = 0.5
-```
-
-Time to wait for network activity to cease. Increase to 3-5s for slower websites. This tracks essential content loading, not dynamic elements like videos.
-
-#### `maximum_wait_page_load_time`
-
-```python
-maximum_wait_page_load_time: float = 5.0
-```
-
-Maximum time to wait for page load before proceeding.
-
-#### `wait_between_actions`
-
-```python
-wait_between_actions: float = 0.5
-```
-
-Time to wait between agent actions.
-
-#### `cookies_file`
-
-```python
-cookies_file: str | None = None
-```
-
-JSON file path to save cookies to.
-
-
-This option is DEPRECATED. Use [`storage_state`](#storage-state) instead, it's the standard playwright format and also supports `localStorage` and `indexedDB`!
-
-The library will automatically save a new `storage_state.json` next to any `cookies_file` path you provide, just use `storage_state='path/to/storage_state.json' to switch to the new format:
-
-`cookies_file.json`: `[{cookie}, {cookie}, {cookie}]`
-⬇️
-`storage_state.json`: `{"cookies": [{cookie}, {cookie}, {cookie}], "origins": {... optional localstorage state ...}}`
-
-Or run `playwright open https://example.com/ --save-storage=storage_state.json` and log into any sites you need to generate a fresh storage state file.
-
-
-
-#### `profile_directory`
-
-```python
-profile_directory: str = 'Default'
-```
-
-Chrome profile subdirectory name inside of your `user_data_dir` (e.g. `Default`, `Profile 1`, `Work`, etc.).
-No need to set this unless you have multiple profiles set up in a single `user_data_dir` and need to use a specific one.
-
-#### `window_position`
-
-```python
-window_position: dict | None = {"width": 0, "height": 0}
-```
-
-Window position from top-left.
-
-
----
-
-
-
-### Playwright Launch Options
-
-
-All the parameters below are standard playwright parameters and can be passed to both `BrowserSession` and `BrowserProfile`.
-They are defined in `browser_use/browser/profile.py`. See here for the [official Playwright documentation](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context) for all of these options.
-
-#### `headless`
-
-```python
-headless: bool | None = None
-```
-
-Runs the browser without a visible UI. If None, auto-detects based on display availability. If you set `headless=False` on a server with no monitor attached, the browser will fail to launch (use `xvfb` + vnc to give a headless server a virtual display you can remote control).
-
-`headless=False` is recommended for maximum stealth and is required for human-in-the-loop workflows.
-
-#### `channel`
-
-```python
-channel: BrowserChannel = 'chromium'
-```
-
-Browser channel: `['chromium']` (default when `stealth=False`), `'chrome'` (default when `stealth=True`), `'chrome-beta'`, `'chrome-dev'`, `'chrome-canary'`, `'msedge'`, `'msedge-beta'`, `'msedge-dev'`, `'msedge-canary'`
-
-Don't worry, other chromium-based browsers not in this list (e.g. `brave`) are still supported if you provide your own [`executable_path`](#executable_path), just set it to `chromium` for those.
-
-#### `executable_path`
-
-```python
-executable_path: str | Path | None = None
-```
-
-Path to browser executable for custom installations.
-
-#### `user_data_dir`
-
-```python
-user_data_dir: str | Path | None = '~/.config/browseruse/profiles/default'
-```
-
-Directory for browser profile data. Set to `None` to use an ephemeral temporary profile (aka incognito mode).
-
-Multiple running browsers **cannot share a single `user_data_dir` at the same time**. You must set it to `None` or
-provide a unique `user_data_dir` per-session if you plan to run multiple browsers.
-
-The browser version run must always be equal to or greater than the version used to create the `user_data_dir`.
-If you see errors like `Failed to parse Extensions` or similar and failures when launching, you're attempting to run an older browser with an incompatible `user_data_dir` that's already been migrated to a newer schema version.
-
-#### `args`
-
-```python
-args: list[str] = []
-```
-Additional command-line arguments to pass to the browser. See here for the [full list of available chrome launch options](https://peter.sh/experiments/chromium-command-line-switches/).
-
-
-
-#### `ignore_default_args`
-
-```python
-ignore_default_args: list[str] | bool = ['--enable-automation', '--disable-extensions']
-```
-
-List of default CLI args to stop playwright from including when launching chrome. Set it to `True` to disable *all* default options (not recommended).
-
-#### `env`
-
-```python
-env: dict[str, str] = {}
-```
-
-Extra environment variables to set when launching browser. e.g. `{'DISPLAY': '1'}` to use a specific X11 display.
-
-#### `chromium_sandbox`
-
-```python
-chromium_sandbox: bool = not IN_DOCKER
-```
-
-Whether to enable Chromium sandboxing (recommended for security). Should always be `False` when running inside Docker
-because Docker provides its own sandboxing can conflict with Chrome's.
-
-#### `devtools`
-
-```python
-devtools: bool = False
-```
-
-Whether to open DevTools panel automatically (only works when `headless=False`).
-
-#### `slow_mo`
-
-```python
-slow_mo: float = 0
-```
-
-Slow down actions by this many milliseconds.
-
-#### `timeout`
-
-```python
-timeout: float = 30000
-```
-
-Default timeout in milliseconds for connecting to a remote browser.
-
-#### `accept_downloads`
-
-```python
-accept_downloads: bool = True
-```
-
-Whether to automatically accept all downloads.
-
-#### `proxy`
-
-```python
-proxy: dict | None = None
-```
-
-Proxy settings. Example: `{"server": "http://proxy.com:8080", "username": "user", "password": "pass"}`.
-
-#### `permissions`
-
-```python
-permissions: list[str] = ['clipboard-read', 'clipboard-write', 'notifications']
-```
-
-Browser permissions to grant. See here for the [full list of available permission](https://playwright.dev/python/docs/api/class-browsercontext#browser-context-grant-permissions).
-
-#### `storage_state`
-
-```python
-storage_state: str | Path | dict | None = None
-```
-
-Browser storage state (cookies, localStorage). Can be file path or dict. See here for the [Playwright `storage_state` documentation](https://playwright.dev/python/docs/api/class-browsercontext#browser-context-storage-state) on how to use it.
-This option is only applied when launching a new browser using the default builtin playwright chromium and `user_data_dir=None` is set.
-
-```bash
-# to create a storage state file, run the following and log into the sites you need once the browser opens:
-playwright open https://example.com/ --save-storage=./storage_state.json
-# then setup a BrowserSession with storage_state='./storage_state.json' and user_data_dir=None to use it
-```
-
-### Playwright Timing Settings
-
-These control how the browser waits for CDP API calls to complete and pages to load.
-
-#### `default_timeout`
-
-```python
-default_timeout: float | None = None
-```
-
-Default timeout for Playwright operations in milliseconds.
-
-#### `default_navigation_timeout`
-
-```python
-default_navigation_timeout: float | None = None
-```
-
-Default timeout for page navigation in milliseconds.
-
-
-### Playwright Viewport Options
-
-Configure browser window size, viewport, and display properties:
-
-#### `user_agent`
-
-```python
-user_agent: str | None = None
-```
-
-Specific user agent to use in this context.
-
-#### `is_mobile`
-
-```python
-is_mobile: bool = False
-```
-
-Whether the meta viewport tag is taken into account and touch events are enabled.
-
-#### `has_touch`
-
-```python
-has_touch: bool = False
-```
-
-Specifies if viewport supports touch events.
-
-#### `geolocation`
-
-```python
-geolocation: dict | None = None
-```
-
-Geolocation coordinates. Example: `{"latitude": 59.95, "longitude": 30.31667}`
-
-#### `locale`
-
-```python
-locale: str | None = None
-```
-
-Specify user locale, for example en-GB, de-DE, etc. Locale will affect the navigator.language value, Accept-Language request header value as well as number and date formatting rules.
-
-#### `timezone_id`
-
-```python
-timezone_id: str | None = None
-```
-
-Timezone identifier (e.g., 'America/New_York').
-
-#### `window_size`
-
-```python
-window_size: dict | None = None
-```
-
-Browser window size for headful mode. Example: `{"width": 1920, "height": 1080}`
-
-#### `viewport`
-
-```python
-viewport: dict | None = None
-```
-
-Viewport size with `width` and `height`. Example: `{"width": 1280, "height": 720}`
-
-#### `no_viewport`
-
-```python
-no_viewport: bool | None = not headless
-```
-
-Disable fixed viewport. Content will resize with window.
-
-*Tip:* don't use this parameter, it's a playwright standard parameter but it's redundant and only serves to override the `viewport` setting above.
-A viewport is *always* used in headless mode regardless of this setting, and is *never* used in headful mode unless you pass `viewport={width, height}` explicitly.
-
-#### `device_scale_factor`
-
-```python
-device_scale_factor: float | None = None
-```
-
-Device scale factor (DPI). Useful for high-resolution screenshots (set it to 2).
-
-#### `screen`
-
-```python
-screen: dict | None = None
-```
-
-Screen size available to browser. Auto-detected if not specified.
-
-#### `color_scheme`
-
-```python
-color_scheme: ColorScheme = 'light'
-```
-
-Preferred color scheme: `'light'`, `'dark'`, `'no-preference'`
-
-#### `contrast`
-
-```python
-contrast: Contrast = 'no-preference'
-```
-
-Contrast preference: `'no-preference'`, `'more'`, `'null'`
-
-#### `reduced_motion`
-
-```python
-reduced_motion: ReducedMotion = 'no-preference'
-```
-
-Reduced motion preference: `'reduce'`, `'no-preference'`, `'null'`
-
-#### `forced_colors`
-
-```python
-forced_colors: ForcedColors = 'none'
-```
-
-Forced colors mode: `'active'`, `'none'`, `'null'`
-
-#### `**playwright.devices[...]`
-
-Playwright provides launch & context arg presets to [emulate common device fingerprints](https://playwright.dev/python/docs/emulation).
-
-```python
-BrowserProfile(
- ...
- **playwright.devices['iPhone 13'], # playwright = await async_playwright().start()
-)
-```
-
-Because `BrowserSession` and `BrowserProfile` take all the standard playwright args, we are able to support these device presets as well.
-
-### Playwright Security Options
-
-> See `allowed_domains` above too!
-
-#### `offline`
-
-```python
-offline: bool = False
-```
-
-Emulate network being offline.
-
-#### `http_credentials`
-
-```python
-http_credentials: dict | None = None
-```
-
-Credentials for HTTP authentication.
-
-#### `extra_http_headers`
-
-```python
-extra_http_headers: dict[str, str] = {}
-```
-
-Additional HTTP headers to be sent with every request.
-
-#### `ignore_https_errors`
-
-```python
-ignore_https_errors: bool = False
-```
-
-Whether to ignore HTTPS errors when sending network requests.
-
-#### `bypass_csp`
-
-```python
-bypass_csp: bool = False
-```
-
-Toggles bypassing Content-Security-Policy.
-
-#### `java_script_enabled`
-
-```python
-java_script_enabled: bool = True
-```
-
-Whether or not to enable JavaScript in the context.
-
-#### `service_workers`
-
-```python
-service_workers: ServiceWorkers = 'allow'
-```
-
-Whether to allow sites to register Service workers: `'allow'`, `'block'`
-
-#### `base_url`
-
-```python
-base_url: str | None = None
-```
-
-Base URL to be used in `page.goto()` and similar operations.
-
-#### `strict_selectors`
-
-```python
-strict_selectors: bool = False
-```
-
-If true, selector passed to Playwright methods will throw if more than one element matches.
-
-#### `client_certificates`
-
-```python
-client_certificates: list[ClientCertificate] = []
-```
-
-Client certificates to be used with requests.
-
-
-### Playwright Recording Options
-
-Note: Browser Use also provides some of our own recording-related options not listed below (see above).
-
-#### `record_video_dir`
-
-
-
-
-```python
-record_video_dir: str | Path | None = None
-```
-
-Directory to save `.webm` video recordings. [Playwright Docs: `record_video_dir`](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context-option-record-video-dir)
-
-
-This parameter also has an alias `save_recording_path` for backwards compatibility with past versions, but we recommend using the standard Playwright name `record_video_dir` going forward.
-
-
-#### `record_video_size`
-
-```python
-record_video_size: dict | None = None. [Playwright Docs: `record_video_size`](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context-option-record-video-size)
-
-```
-
-Video size. Example: `{"width": 1280, "height": 720}`
-
-#### `record_har_path`
-
-
-
-
-```python
-record_har_path: str | Path | None = None
-```
-
-Path to save `.har` network trace files. [Playwright Docs: `record_har_path`](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context-option-record-har-path)
-
-
-This parameter also has an alias `save_har_path` for backwards compatibility with past versions, but we recommend using the standard Playwright name `record_har_path` going forward.
-
-
-#### `record_har_content`
-
-```python
-record_har_content: RecordHarContent = 'embed'
-```
-
-How to persist HAR content: `'omit'`, `'embed'`, `'attach'`
-
-#### `record_har_mode`
-
-```python
-record_har_mode: RecordHarMode = 'full'
-```
-
-HAR recording mode: `'full'`, `'minimal'`
-
-#### `record_har_omit_content`
-
-```python
-record_har_omit_content: bool = False
-```
-
-Whether to omit request content from the HAR.
-
-#### `record_har_url_filter`
-
-```python
-record_har_url_filter: str | Pattern | None = None
-```
-
-URL filter for HAR recording.
-
-#### `downloads_path`
-
-```python
-downloads_path: str | Path | None = '~/.config/browseruse/downloads'
-```
-
-(aliases: `downloads_dir`, `save_downloads_path`)
-
-Local filesystem directory to save browser file downloads to.
-
-#### `traces_dir`
-
-
-
-
-```python
-traces_dir: str | Path | None = None
-```
-
-Directory to save all-in-one trace files. Files are automatically named as `{traces_dir}/{context_id}.zip`. [Playwright Docs: `traces_dir`](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context-option-traces-dir)
-
-
-This parameter also has an alias `trace_path` for backwards compatibility with past versions, but we recommend using the standard Playwright name `traces_dir` going forward.
-
-
-#### `handle_sighup`
-
-```python
-handle_sighup: bool = True
-```
-
-Whether playwright should swallow SIGHUP signals and kill the browser.
-
-#### `handle_sigint`
-
-```python
-handle_sigint: bool = False
-```
-
-Whether playwright should swallow SIGINT signals and kill the browser.
-
-#### `handle_sigterm`
-
-```python
-handle_sigterm: bool = False
-```
-
-Whether playwright should swallow SIGTERM signals and kill the browser.
-
----
-
-## Full Example
-
-```python
-from browser_use import BrowserSession, BrowserProfile, Agent
-
-browser_profile = BrowserProfile(
- headless=False,
- storage_state="path/to/storage_state.json",
- wait_for_network_idle_page_load_time=3.0,
- viewport={"width": 1280, "height": 1100},
- locale='en-US',
- user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36',
- highlight_elements=True,
- viewport_expansion=500,
- allowed_domains=['*.google.com', 'http*://*.wikipedia.org'],
- user_data_dir=None,
-)
-
-browser_session = BrowserSession(
- browser_profile=browser_profile,
- headless=True, # extra kwargs to the session override the defaults in the profile
-)
-
-# you can drive a session without the agent / reuse it between agents
-await browser_session.start()
-page = await browser_session.get_current_page()
-await page.goto('https://example.com/first/page')
-
-async def run_search():
- agent = Agent(
- task='Your task',
- llm=llm,
- page=page, # optional: pass a specific playwright page to start on
- browser_session=browser_session, # optional: pass an existing browser session to an agent
- )
-```
-
----
-
-## Summary
-
-- **BrowserSession** (defined in `browser_use/browser/session.py`) handles the live browser connection and runtime state
-- **BrowserProfile** (defined in `browser_use/browser/profile.py`) is a template that can store default config parameters for a `BrowserSession(...)`
-
-Configuration parameters defined in both scopes consumed by these calls depending on whether we're connecting/launching:
-
-- `BrowserConnectArgs` - args for `playwright.BrowserType.connect_over_cdp(...)`
-- `BrowserLaunchArgs` - args for `playwright.BrowserType.launch(...)`
-- `BrowserNewContextArgs` - args for `playwright.BrowserType.new_context(...)`
-- `BrowserLaunchPersistentContextArgs` - args for `playwright.BrowserType.launch_persistent_context(...)`
-- Browser Use's own internal methods
-
-For more details on Playwright's browser context options, see their [launch args documentation](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context).
-
----
diff --git a/.github/instructions/browser-use.instructions.md b/.github/instructions/browser-use.instructions.md
deleted file mode 100644
index d424b8e..0000000
--- a/.github/instructions/browser-use.instructions.md
+++ /dev/null
@@ -1,82 +0,0 @@
----
-applyTo: '**'
----
-## 🧠 General Guidelines for Contributing to `browser-use`
-
-**Browser-Use** is an AI agent that autonomously interacts with the web. It takes a user-defined task, navigates web pages using Chromium via Playwright, processes HTML, and repeatedly queries a language model (like `gpt-4o`) to decide the next action—until the task is completed.
-
-### 🗂️ File Documentation
-
-When you create a **new file**:
-
-* **For humans**: At the top of the file, include a docstring in natural language explaining:
-
- * What this file does.
- * How it fits into the browser-use system.
- * If it introduces a new abstraction or replaces an old one.
-* **For LLMs/AI**: Include structured metadata using standardized comments such as:
-
- ```python
- # @file purpose: Defines
- ```
-
----
-
-### 🧰 Development Rules
-
-* ✅ **Always use [`uv`](mdc:https:/github.com/astral-sh/uv) instead of `pip`**
- For deterministic and fast dependency installs.
-
-```bash
-uv venv --python 3.11
-source .venv/bin/activate
-uv sync
-```
-
-* ✅ **Use real model names**
- Do **not** replace `gpt-4o` with `gpt-4`. The model `gpt-4o` is a distinct release and supported.
-
-* ✅ **Type-safe coding**
- Use **Pydantic v2 models** for all internal action schemas, task inputs/outputs, and controller I/O. This ensures robust validation and LLM-call integrity.
-
----
-
-## ⚙️ Adding New Actions
-
-To add a new action that your browser agent can execute:
-
-```python
-from playwright.async_api import Page
-from browser_use.core.controller import Controller, ActionResult
-
-controller = Controller()
-
-@controller.registry.action("Search the web for a specific query")
-async def search_web(query: str, page: Page):
- # Implement your logic here, e.g., query a search engine and return results
- result = ...
- return ActionResult(extracted_content=result, include_in_memory=True)
-```
-
-### Notes:
-
-* Use descriptive names and docstrings for each action.
-* Prefer returning `ActionResult` with structured content to help the agent reason better.
-
----
-
-## 🧠 Creating and Running an Agent
-
-To define a task and run a browser-use agent:
-
-```python
-from browser_use import Agent
-from langchain.chat_models import ChatOpenAI
-
-task = "Find the CEO of OpenAI and return their name"
-model = ChatOpenAI(model="gpt-4o")
-
-agent = Agent(task=task, llm=model, controller=controller)
-
-history = await agent.run()
-```
\ No newline at end of file
diff --git a/.github/instructions/custom-functions.instructions.md b/.github/instructions/custom-functions.instructions.md
deleted file mode 100644
index 3280715..0000000
--- a/.github/instructions/custom-functions.instructions.md
+++ /dev/null
@@ -1,249 +0,0 @@
----
-description: "Extend default agent and write custom action functions to do certain tasks"
-applyTo: '**'
----
-
-Custom actions are functions *you* provide, that are added to our [default actions](https://github.com/browser-use/browser-use/blob/main/browser_use/controller/service.py) the agent can use to accomplish tasks.
-Action functions can request [arbitrary parameters](#action-parameters-via-pydantic-model) that the LLM has to come up with + a fixed set of [framework-provided arguments](#framework-provided-parameters) for browser APIs / `Agent(context=...)` / etc.
-
-
- Our default set of actions is already quite powerful, the built-in `Controller` provides basics like `open_tab`, `scroll_down`, `extract_content`, [and more](https://github.com/browser-use/browser-use/blob/main/browser_use/controller/service.py).
-
-
-It's easy to add your own actions to implement additional custom behaviors, integrations with other apps, or performance optimizations.
-
-For examples of custom actions (e.g. uploading files, asking a human-in-the-loop for help, drawing a polygon with the mouse, and more), see [examples/custom-functions](https://github.com/browser-use/browser-use/tree/main/examples/custom-functions).
-
-
-## Action Function Registration
-
-To register your own custom functions (which can be `sync` or `async`), decorate them with the `@controller.action(...)` decorator. This saves them into the `controller.registry`.
-
-```python
-from browser_use import Controller, ActionResult
-
-controller = Controller()
-
-@controller.action('Ask human for help with a question', domains=['example.com']) # pass allowed_domains= or page_filter= to limit actions to certain pages
-def ask_human(question: str) -> ActionResult:
- answer = input(f'{question} > ')
- return ActionResult(extracted_content=f'The human responded with: {answer}', include_in_memory=True)
-```
-
-```python
-# Then pass your controller to the agent to use it
-agent = Agent(
- task='...',
- llm=llm,
- controller=controller,
-)
-```
-
-
- Keep your action function names and descriptions short and concise:
- - The LLM chooses between actions to run solely based on the function name and description
- - The LLM decides how to fill action params based on their names, type hints, & defaults
-
-
----
-
-## Action Parameters
-
-Browser Use supports two patterns for defining action parameters: normal function arguments, or a Pydantic model.
-
-### Function Arguments
-
-For simple actions that don't need default values, you can define the action parameters directly as arguments to the function. This one takes a single string argument, `css_selector`.
-When the LLM calls an action, it sees its argument names & types, and will provide values that fit.
-
-```python
-@controller.action('Click element')
-def click_element(css_selector: str, page: Page) -> ActionResult:
- # css_selector is an action param the LLM must provide when calling
- # page is a special framework-provided param to access the browser APIs (see below)
- await page.locator(css_selector).click()
- return ActionResult(extracted_content=f"Clicked element {css_selector}")
-```
-
-### Pydantic Model
-
-You can define a pydantic model for the parameters your action expects by setting a `@controller.action(..., param_model=MyParams)`.
-This allows you to use optional parameters, default values, `Annotated[...]` types with custom validation, field descriptions, and other features offered by pydantic.
-
-When the agent calls calls your agent function, an instance of your model with the values filled by the LLM will be passed as the argument named `params` to your action function.
-
-Using a pydantic model is helpful because it allows more flexibility and power to enforce the schema of the values the LLM should provide.
-The LLM gets the entire pydantic JSON schema for your `param_model`, it will see the function name & description + individual field names, types, descriptions, and default values.
-
-
-```python
-from typing import Annotated
-from pydantic import BaseModel, AfterValidator
-from browser_use import ActionResult
-
-class MyParams(BaseModel):
- field1: int
- field2: str = 'default value'
- field3: Annotated[str, AfterValidator(lambda s: s.lower())] # example: enforce always lowercase
- field4: str = Field(default='abc', description='Detailed description for the LLM')
-
-@controller.action('My action', param_model=MyParams)
-def my_action(params: MyParams, page: Page) -> ActionResult:
- await page.keyboard.type(params.field2)
- return ActionResult(extracted_content=f"Inputted {params} on {page.url}")
-```
-
-Any special framework-provided arguments (e.g. `page`) will be passed as separate positional arguments after `params`.
-
-
-To use a `BaseModel` the arg *must* be called `params`. Action function args are matched and filled like named arguments; arg order doesn't matter but names and types do.
-
-
-### Framework-Provided Parameters
-
-These special action parameters are injected by the `Controller` and are passed as extra args to any actions that expect them.
-
-For example, actions that need to run playwright code to interact with the browser should take the argument `page` or `browser_session`.
-
-- `page: Page` - The current Playwright page (shortcut for `browser_session.get_current_page()`)
-- `browser_session: BrowserSession` - The current browser session (and playwright context via `browser_session.browser_context`)
-- `context: AgentContext` - Any optional top-level context object passed to the Agent, e.g. `Agent(context=user_provided_obj)`
-- `page_extraction_llm: BaseChatModel` - LLM instance used for page content extraction
-- `available_file_paths: list[str]` - List of available file paths for upload / processing
-- `has_sensitive_data: bool` - Whether the action content contains sensitive data markers (check this to avoid logging sensitive data to terminal by accident)
-
-#### Example: Action uses the current `page`
-
-```python
-from playwright.async_api import Page
-from browser_use import Controller, ActionResult
-
-controller = Controller()
-
-@controller.action('Type keyboard input into a page')
-async def input_text_into_page(text: str, page: Page) -> ActionResult:
- await page.keyboard.type(text)
- return ActionResult(extracted_content='Website opened')
-```
-
-#### Example: Action uses the `browser_context`
-
-```python
-from browser_use import BrowserSession, Controller, ActionResult
-
-controller = Controller()
-
-@controller.action('Open website')
-async def open_website(url: str, browser_session: BrowserSession) -> ActionResult:
- # find matching existing tab by looking through all pages in playwright browser_context
- all_tabs = await browser_session.browser_context.pages
- for tab in all_tabs:
- if tab.url == url:
- await tab.bring_to_foreground()
- return ActionResult(extracted_content=f'Switched to tab with url {url}')
- # otherwise, create a new tab
- new_tab = await browser_session.browser_context.new_page()
- await new_tab.goto(url)
- return ActionResult(extracted_content=f'Opened new tab with url {url}')
-```
-
-
----
-
-
-## Important Rules
-
-1. **Return an [`ActionResult`](https://github.com/search?q=repo%3Abrowser-use%2Fbrowser-use+%22class+ActionResult%28BaseModel%29%22&type=code)**: All actions should return an `ActionResult | str | None`. The stringified version of the result is passed back to the LLM, and optionally persisted in the long-term memory when `ActionResult(..., include_in_memory=True)`.
-2. **Type hints on arguments are required**: They are used to verify that action params don't conflict with special arguments injected by the controller (e.g. `page`)
-3. **Actions functions called directly must be passed kwargs**: When calling actions from other actions or python code, you must **pass all parameters as kwargs only**, even though the actions are usually defined using positional args (for the same reasons as [pluggy](https://pluggy.readthedocs.io/en/stable/index.html#calling-hooks)).
- Action arguments are always matched by name and type, **not** positional order, so this helps prevent ambiguity / reordering issues while keeping action signatures short.
- ```python
- @controller.action('Fill in the country form field')
- def input_country_field(country: str, page: Page) -> ActionResult:
- await some_action(123, page=page) # ❌ not allowed: positional args, use kwarg syntax when calling
- await some_action(abc=123, page=page) # ✅ allowed: action params & special kwargs
- await some_other_action(params=OtherAction(abc=123), page=page) # ✅ allowed: params=model & special kwargs
- ```
-
-```python
-# Using Pydantic Model to define action params (recommended)
-class PinCodeParams(BaseModel):
- code: int
- retries: int = 3 # ✅ supports optional/defaults
-
-@controller.action('...', param_model=PinCodeParams)
-async def input_pin_code(params: PinCodeParams, page: Page): ... # ✅ special params at the end
-
-# Using function arguments to define action params
-async def input_pin_code(code: int, retries: int, page: Page): ... # ✅ params first, special params second, no defaults
-async def input_pin_code(code: int, retries: int=3): ... # ✅ defaults ok only if no special params needed
-async def input_pin_code(code: int, retries: int=3, page: Page): ... # ❌ Python SyntaxError! not allowed
-```
-
-
----
-
-
-## Reusing Custom Actions Across Agents
-
-You can use the same controller for multiple agents.
-
-```python
-controller = Controller()
-
-# ... register actions to the controller
-
-agent = Agent(
- task="Go to website X and find the latest news",
- llm=llm,
- controller=controller
-)
-
-# Run the agent
-await agent.run()
-
-agent2 = Agent(
- task="Go to website Y and find the latest news",
- llm=llm,
- controller=controller
-)
-
-await agent2.run()
-```
-
-
- The controller is stateless and can be used to register multiple actions and
- multiple agents.
-
-
-
-
-## Exclude functions
-
-If you want to exclude some registered actions and make them unavailable to the agent, you can do:
-```python
-controller = Controller(exclude_actions=['open_tab', 'search_google'])
-agent = Agent(controller=controller, ...)
-```
-
-
-If you want actions to only be available on certain pages, and to not tell the LLM about them on other pages,
- you can use the `allowed_domains` and `page_filter`:
-
-```python
-from pydantic import BaseModel
-from browser_use import Controller, ActionResult
-
-controller = Controller()
-
-async def is_ai_allowed(page: Page):
- if api.some_service.check_url(page.url):
- logger.warning('Allowing AI agent to visit url:', page.url)
- return True
- return False
-
-@controller.action('Fill out secret_form', allowed_domains=['https://*.example.com'], page_filter=is_ai_allowed)
-def fill_out_form(...) -> ActionResult:
- ... will only be runnable by LLM on pages that match https://*.example.com *AND* where is_ai_allowed(page) returns True
-
-```
diff --git a/.github/instructions/hooks.instructions.md b/.github/instructions/hooks.instructions.md
deleted file mode 100644
index eeb8880..0000000
--- a/.github/instructions/hooks.instructions.md
+++ /dev/null
@@ -1,381 +0,0 @@
----
-description: "Customize agent behavior with lifecycle hooks"
-applyTo: '**'
----
-
-Browser-Use provides lifecycle hooks that allow you to execute custom code at specific points during the agent's execution.
-Hook functions can be used to read and modify agent state while running, implement custom logic, change configuration, integrate the Agent with external applications.
-
-
-## Available Hooks
-
-Currently, Browser-Use provides the following hooks:
-
-| Hook | Description | When it's called |
-| ---- | ----------- | ---------------- |
-| `on_step_start` | Executed at the beginning of each agent step | Before the agent processes the current state and decides on the next action |
-| `on_step_end` | Executed at the end of each agent step | After the agent has executed all the actions for the current step, before it starts the next step |
-
-```python
-await agent.run(on_step_start=..., on_step_end=...)
-```
-
-Each hook should be an `async` callable function that accepts the `agent` instance as its only parameter.
-
-
-### Basic Example
-
-```python
-from browser_use import Agent
-from langchain_openai import ChatOpenAI
-
-
-async def my_step_hook(agent: Agent):
- # inside a hook you can access all the state and methods under the Agent object:
- # agent.settings, agent.state, agent.task
- # agent.controller, agent.llm, agent.browser_session
- # agent.pause(), agent.resume(), agent.add_new_task(...), etc.
-
- # You also have direct access to the playwright Page and Browser Context
- page = await agent.browser_session.get_current_page()
- # https://playwright.dev/python/docs/api/class-page
-
- current_url = page.url
- visit_log = agent.state.history.urls()
- previous_url = visit_log[-2] if len(visit_log) >= 2 else None
- print(f"Agent was last on URL: {previous_url} and is now on {current_url}")
-
- # Example: listen for events on the page, interact with the DOM, run JS directly, etc.
- await page.on('domcontentloaded', lambda: print('page navigated to a new url...'))
- await page.locator("css=form > input[type=submit]").click()
- await page.evaluate('() => alert(1)')
- await page.browser.new_tab
- await agent.browser_session.session.context.add_init_script('/* some JS to run on every page */')
-
- # Example: monitor or intercept all network requests
- async def handle_request(route):
- # Print, modify, block, etc. do anything to the requests here
- # https://playwright.dev/python/docs/network#handle-requests
- print(route.request, route.request.headers)
- await route.continue_(headers=route.request.headers)
- await page.route("**/*", handle_route)
-
- # Example: pause agent execution and resume it based on some custom code
- if '/completed' in current_url:
- agent.pause()
- Path('result.txt').write_text(await page.content())
- input('Saved "completed" page content to result.txt, press [Enter] to resume...')
- agent.resume()
-
-agent = Agent(
- task="Search for the latest news about AI",
- llm=ChatOpenAI(model="gpt-4o"),
-)
-
-await agent.run(
- on_step_start=my_step_hook,
- # on_step_end=...
- max_steps=10
-)
-```
-
-## Data Available in Hooks
-
-When working with agent hooks, you have access to the entire `Agent` instance. Here are some useful data points you can access:
-
-- `agent.task` lets you see what the main task is, `agent.add_new_task(...)` lets you queue up a new one
-- `agent.controller` give access to the `Controller()` object and `Registry()` containing the available actions
- - `agent.controller.registry.execute_action('click_element_by_index', {'index': 123}, browser_session=agent.browser_session)`
-- `agent.context` lets you access any user-provided context object passed in to `Agent(context=...)`
-- `agent.sensitive_data` contains the sensitive data dict, which can be updated in-place to add/remove/modify items
-- `agent.settings` contains all the configuration options passed to the `Agent(...)` at init time
-- `agent.llm` gives direct access to the main LLM object (e.g. `ChatOpenAI`)
-- `agent.state` gives access to lots of internal state, including agent thoughts, outputs, actions, etc.
- - `agent.state.history.model_thoughts()`: Reasoning from Browser Use's model.
- - `agent.state.history.model_outputs()`: Raw outputs from the Browsre Use's model.
- - `agent.state.history.model_actions()`: Actions taken by the agent
- - `agent.state.history.extracted_content()`: Content extracted from web pages
- - `agent.state.history.urls()`: URLs visited by the agent
-- `agent.browser_session` gives direct access to the `BrowserSession()` and playwright objects
- - `agent.browser_session.get_current_page()`: Get the current playwright `Page` object the agent is focused on
- - `agent.browser_session.browser_context`: Get the current playwright `BrowserContext` object
- - `agent.browser_session.browser_context.pages`: Get all the tabs currently open in the context
- - `agent.browser_session.get_page_html()`: Current page HTML
- - `agent.browser_session.take_screenshot()`: Screenshot of the current page
-
-
-## Tips for Using Hooks
-
-- **Avoid blocking operations**: Since hooks run in the same execution thread as the agent, try to keep them efficient or use asynchronous patterns.
-- **Handle exceptions**: Make sure your hook functions handle exceptions gracefully to prevent interrupting the agent's main flow.
-- **Use custom actions instead**: hooks are fairly advanced, most things can be implemented with [custom action functions](/customize/custom-functions) instead
-
----
-
-## Complex Example: Agent Activity Recording System
-
-This comprehensive example demonstrates a complete implementation for recording and saving Browser-Use agent activity, consisting of both server and client components.
-
-### Setup Instructions
-
-To use this example, you'll need to:
-
-1. Set up the required dependencies:
- ```bash
- pip install fastapi uvicorn prettyprinter pyobjtojson dotenv browser-use langchain-openai
- ```
-
-2. Create two separate Python files:
- - `api.py` - The FastAPI server component
- - `client.py` - The Browser-Use agent with recording hook
-
-3. Run both components:
- - Start the API server first: `python api.py`
- - Then run the client: `python client.py`
-
-### Server Component (api.py)
-
-The server component handles receiving and storing the agent's activity data:
-
-```python
-#!/usr/bin/env python3
-
-#
-# FastAPI API to record and save Browser-Use activity data.
-# Save this code to api.py and run with `python api.py`
-#
-
-import json
-import base64
-from pathlib import Path
-
-from fastapi import FastAPI, Request
-import prettyprinter
-import uvicorn
-
-prettyprinter.install_extras()
-
-# Utility function to save screenshots
-def b64_to_png(b64_string: str, output_file):
- """
- Convert a Base64-encoded string to a PNG file.
-
- :param b64_string: A string containing Base64-encoded data
- :param output_file: The path to the output PNG file
- """
- with open(output_file, "wb") as f:
- f.write(base64.b64decode(b64_string))
-
-# Initialize FastAPI app
-app = FastAPI()
-
-
-@app.post("/post_agent_history_step")
-async def post_agent_history_step(request: Request):
- data = await request.json()
- prettyprinter.cpprint(data)
-
- # Ensure the "recordings" folder exists using pathlib
- recordings_folder = Path("recordings")
- recordings_folder.mkdir(exist_ok=True)
-
- # Determine the next file number by examining existing .json files
- existing_numbers = []
- for item in recordings_folder.iterdir():
- if item.is_file() and item.suffix == ".json":
- try:
- file_num = int(item.stem)
- existing_numbers.append(file_num)
- except ValueError:
- # In case the file name isn't just a number
- pass
-
- if existing_numbers:
- next_number = max(existing_numbers) + 1
- else:
- next_number = 1
-
- # Construct the file path
- file_path = recordings_folder / f"{next_number}.json"
-
- # Save the JSON data to the file
- with file_path.open("w") as f:
- json.dump(data, f, indent=2)
-
- # Optionally save screenshot if needed
- # if "website_screenshot" in data and data["website_screenshot"]:
- # screenshot_folder = Path("screenshots")
- # screenshot_folder.mkdir(exist_ok=True)
- # b64_to_png(data["website_screenshot"], screenshot_folder / f"{next_number}.png")
-
- return {"status": "ok", "message": f"Saved to {file_path}"}
-
-if __name__ == "__main__":
- print("Starting Browser-Use recording API on http://0.0.0.0:9000")
- uvicorn.run(app, host="0.0.0.0", port=9000)
-```
-
-### Client Component (client.py)
-
-The client component runs the Browser-Use agent with a recording hook:
-
-```python
-#!/usr/bin/env python3
-
-#
-# Client to record and save Browser-Use activity.
-# Save this code to client.py and run with `python client.py`
-#
-
-import asyncio
-import requests
-from dotenv import load_dotenv
-from pyobjtojson import obj_to_json
-from langchain_openai import ChatOpenAI
-from browser_use import Agent
-
-# Load environment variables (for API keys)
-load_dotenv()
-
-
-def send_agent_history_step(data):
- """Send the agent step data to the recording API"""
- url = "http://127.0.0.1:9000/post_agent_history_step"
- response = requests.post(url, json=data)
- return response.json()
-
-
-async def record_activity(agent_obj):
- """Hook function that captures and records agent activity at each step"""
- website_html = None
- website_screenshot = None
- urls_json_last_elem = None
- model_thoughts_last_elem = None
- model_outputs_json_last_elem = None
- model_actions_json_last_elem = None
- extracted_content_json_last_elem = None
-
- print('--- ON_STEP_START HOOK ---')
-
- # Capture current page state
- website_html = await agent_obj.browser_session.get_page_html()
- website_screenshot = await agent_obj.browser_session.take_screenshot()
-
- # Make sure we have state history
- if hasattr(agent_obj, "state"):
- history = agent_obj.state.history
- else:
- history = None
- print("Warning: Agent has no state history")
- return
-
- # Process model thoughts
- model_thoughts = obj_to_json(
- obj=history.model_thoughts(),
- check_circular=False
- )
- if len(model_thoughts) > 0:
- model_thoughts_last_elem = model_thoughts[-1]
-
- # Process model outputs
- model_outputs = agent_obj.state.history.model_outputs()
- model_outputs_json = obj_to_json(
- obj=model_outputs,
- check_circular=False
- )
- if len(model_outputs_json) > 0:
- model_outputs_json_last_elem = model_outputs_json[-1]
-
- # Process model actions
- model_actions = agent_obj.state.history.model_actions()
- model_actions_json = obj_to_json(
- obj=model_actions,
- check_circular=False
- )
- if len(model_actions_json) > 0:
- model_actions_json_last_elem = model_actions_json[-1]
-
- # Process extracted content
- extracted_content = agent_obj.state.history.extracted_content()
- extracted_content_json = obj_to_json(
- obj=extracted_content,
- check_circular=False
- )
- if len(extracted_content_json) > 0:
- extracted_content_json_last_elem = extracted_content_json[-1]
-
- # Process URLs
- urls = agent_obj.state.history.urls()
- urls_json = obj_to_json(
- obj=urls,
- check_circular=False
- )
- if len(urls_json) > 0:
- urls_json_last_elem = urls_json[-1]
-
- # Create a summary of all data for this step
- model_step_summary = {
- "website_html": website_html,
- "website_screenshot": website_screenshot,
- "url": urls_json_last_elem,
- "model_thoughts": model_thoughts_last_elem,
- "model_outputs": model_outputs_json_last_elem,
- "model_actions": model_actions_json_last_elem,
- "extracted_content": extracted_content_json_last_elem
- }
-
- print("--- MODEL STEP SUMMARY ---")
- print(f"URL: {urls_json_last_elem}")
-
- # Send data to the API
- result = send_agent_history_step(data=model_step_summary)
- print(f"Recording API response: {result}")
-
-
-async def run_agent():
- """Run the Browser-Use agent with the recording hook"""
- agent = Agent(
- task="Compare the price of gpt-4o and DeepSeek-V3",
- llm=ChatOpenAI(model="gpt-4o"),
- )
-
- try:
- print("Starting Browser-Use agent with recording hook")
- await agent.run(
- on_step_start=record_activity,
- max_steps=30
- )
- except Exception as e:
- print(f"Error running agent: {e}")
-
-
-if __name__ == "__main__":
- # Check if API is running
- try:
- requests.get("http://127.0.0.1:9000")
- print("Recording API is available")
- except:
- print("Warning: Recording API may not be running. Start api.py first.")
-
- # Run the agent
- asyncio.run(run_agent())
-```
-
-Contribution by Carlos A. Planchón.
-
-### Working with the Recorded Data
-
-After running the agent, you'll find the recorded data in the `recordings` directory. Here's how you can use this data:
-
-1. **View recorded sessions**: Each JSON file contains a snapshot of agent activity for one step
-2. **Extract screenshots**: You can modify the API to save screenshots separately
-3. **Analyze agent behavior**: Use the recorded data to study how the agent navigates websites
-
-### Extending the Example
-
-You can extend this recording system in several ways:
-
-1. **Save screenshots separately**: Uncomment the screenshot saving code in the API
-2. **Add a web dashboard**: Create a simple web interface to view recorded sessions
-3. **Add session IDs**: Modify the API to group steps by agent session
-4. **Add filtering**: Implement filters to record only specific types of actions
diff --git a/.github/instructions/output-format.instructions.md b/.github/instructions/output-format.instructions.md
deleted file mode 100644
index 19613c8..0000000
--- a/.github/instructions/output-format.instructions.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-description: "The default is text. But you can define a structured output format to make post-processing easier."
-applyTo: '**'
----
-
-## Custom output format
-With [this example](https://github.com/browser-use/browser-use/blob/main/examples/features/custom_output.py) you can define what output format the agent should return to you.
-
-```python
-from pydantic import BaseModel
-# Define the output format as a Pydantic model
-class Post(BaseModel):
- post_title: str
- post_url: str
- num_comments: int
- hours_since_post: int
-
-
-class Posts(BaseModel):
- posts: List[Post]
-
-
-controller = Controller(output_model=Posts)
-
-
-async def main():
- task = 'Go to hackernews show hn and give me the first 5 posts'
- model = ChatOpenAI(model='gpt-4o')
- agent = Agent(task=task, llm=model, controller=controller)
-
- history = await agent.run()
-
- result = history.final_result()
- if result:
- parsed: Posts = Posts.model_validate_json(result)
-
- for post in parsed.posts:
- print('\n--------------------------------')
- print(f'Title: {post.post_title}')
- print(f'URL: {post.post_url}')
- print(f'Comments: {post.num_comments}')
- print(f'Hours since post: {post.hours_since_post}')
- else:
- print('No result')
-
-
-if __name__ == '__main__':
- asyncio.run(main())
-```
diff --git a/.github/instructions/real-browser.instructions.md b/.github/instructions/real-browser.instructions.md
deleted file mode 100644
index ce11d64..0000000
--- a/.github/instructions/real-browser.instructions.md
+++ /dev/null
@@ -1,414 +0,0 @@
----
-description: "Connect to a remote browser or launch a new local browser."
-applyTo: '**'
----
-
-## Overview
-
-Browser Use supports a wide variety of ways to launch or connect to a browser:
-
-- Launch a new local browser using playwright/patchright chromium (the default)
-- Connect to a remote browser using CDP or WSS
-- Use an existing playwright `Page`, `Browser`, or `BrowserContext` object
-- Connect to a local browser already running using `browser_pid`
-
-
-Don't want to manage your own browser infrastructure? Try [☁️ Browser Use Cloud](https://browser-use.com) ➡️
-
-We provide automatic CAPTCHA solving, proxies, human-in-the-loop automation, and more!
-
-
-## Connection Methods
-
-### Method A: Launch a New Local Browser (Default)
-
-Launch a local browser using built-in default (playwright `chromium`) or a provided `executable_path`:
-
-```python
-from browser_use import Agent, BrowserSession
-
-# If no executable_path provided, uses Playwright/Patchright's built-in Chromium
-browser_session = BrowserSession(
- # Path to a specific Chromium-based executable (optional)
- executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', # macOS
- # For Windows: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
- # For Linux: '/usr/bin/google-chrome'
-
- # Use a specific data directory on disk (optional, set to None for incognito)
- user_data_dir='~/.config/browseruse/profiles/default', # this is the default
- # ... any other BrowserProfile or playwright launch_persistnet_context config...
- # headless=False,
-)
-
-agent = Agent(
- task="Your task here",
- llm=llm,
- browser_session=browser_session,
-)
-```
-
-We support most `chromium`-based browsers in `executable_path`, including [Brave](https://github.com/browser-use/browser-use/tree/main/examples/browser/stealth.py), [patchright chromium](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright), [rebrowser](https://rebrowser.net/), Edge, and more. See [`examples/browser/stealth.py`](https://github.com/browser-use/browser-use/tree/main/examples/browser) for more. We do not support Firefox or Safari at the moment.
-
-
- [As of Chrome v136](https://github.com/browser-use/browser-use/issues/1520), driving browsers with the default profile is [no longer supported](https://developer.chrome.com/blog/remote-debugging-port) for security reasons. Browser-Use has transitioned to creating a new dedicated profile for agents in: `~/.config/browseruse/profiles/default`. You can [open this profile](https://superuser.com/questions/377186/how-do-i-start-chrome-using-a-specified-user-profile) and log into everything you need your agent to have access to, and it will persist over time.
-
-
-### Method B: Connect Using Existing Playwright Objects
-
-Pass existing Playwright `Page`, `BrowserContext`, `Browser`, and/or `playwright` API object to `BrowserSession(...)`:
-
-```python
-from browser_use import Agent, BrowserSession
-from playwright.async_api import async_playwright
-# from patchright.async_api import async_playwright # stealth alternative
-
-async with async_playwright() as playwright:
- browser = await playwright.chromium.launch()
- context = await browser.new_context()
- page = await context.new_page()
-
- browser_session = BrowserSession(
- page=page,
- # browser_context=context, # all these are supported
- # browser=browser,
- # playwright=playwright,
- )
-
- agent = Agent(
- task="Your task here",
- llm=llm,
- browser_session=browser_session,
- )
-```
-
-You can also pass `page` directly to `Agent(...)` as a shortcut.
-
-```python
-agent = Agent(
- task="Your task here",
- llm=llm,
- page=page,
-)
-```
-
-### Method C: Connect to Local Browser Using Browser PID
-
-Connect to a browser with open `--remote-debugging-port`:
-
-```python
-from browser_use import Agent, BrowserSession
-
-# First, start Chrome with remote debugging:
-# /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9242
-
-# Then connect using the process ID
-browser_session = BrowserSession(browser_pid=12345) # Replace with actual Chrome PID
-
-agent = Agent(
- task="Your task here",
- llm=llm,
- browser_session=browser_session,
-)
-```
-
-### Method D: Connect to remote Playwright Node.js Browser Server via WSS URL
-
-Connect to Playwright Node.js server providers:
-
-```python
-from browser_use import Agent, BrowserSession
-
-# Connect to a playwright server
-browser_session = BrowserSession(wss_url="wss://your-playwright-server.com/ws")
-
-agent = Agent(
- task="Your task here",
- llm=llm,
- browser_session=browser_session,
-)
-```
-
-### Method E: Connect to Remote Browser via CDP URL
-
-Connect to any remote Chromium-based browser:
-
-```python
-from browser_use import Agent, BrowserSession
-
-# Connect to Chrome via CDP
-browser_session = BrowserSession(cdp_url="http://localhost:9222")
-
-agent = Agent(
- task="Your task here",
- llm=llm,
- browser_session=browser_session,
-)
-```
-
-
-
-## Security Considerations
-
-
- When using any browser profile, the agent will have access to:
- - All its logged-in sessions and cookies
- - Saved passwords (if autofill is enabled)
- - Browser history and bookmarks
- - Extensions and their data
-
- Always review the task you're giving to the agent and ensure it aligns with your security requirements!
- Use `Agent(sensitive_data={'https://auth.example.com': {x_key: value}})` for any secrets, and restrict the browser with `BrowserSession(allowed_domains=['https://*.example.com'])`.
-
-
-## Best Practices
-
-1. **Use isolated profiles**: Create separate Chrome profiles for different agents to limit scope of risk:
- ```python
- browser_session = BrowserSession(
- user_data_dir='~/.config/browseruse/profiles/banking',
- # profile_directory='Default'
- )
- ```
-
-2. **Limit domain access**: Restrict which sites the agent can visit:
- ```python
- browser_session = BrowserSession(
- allowed_domains=['example.com', 'http*://*.github.com'],
- )
- ```
-
-3. **Enable `keep_alive=True`** If you want to use a single `BrowserSession` with more than one agent:
- ```python
- browser_session = BrowserSession(
- keep_alive=True,
- ...
- )
- await browser_session.start() # start the session yourself before passing to Agent
- ...
- agent = Agent(..., browser_session=browser_session)
- await agent.run()
- ...
- await browser_session.kill() # end the session yourself, shortcut for keep_alive=False + .stop()
- ```
-
-## Re-Using a Browser
-
-A `BrowserSession` starts when the browser is launched/connected, and ends when the browser process exits/disconnects. A session internally manages a single live playwright browser context, and is normally auto-closed by the agent when its task is complete (*if* the agent started the session itself). If you pass an existing `BrowserSession` into an Agent, or if you set `BrowserSession(keep_alive=True)`, the session will not be closed and can be re-used between agents.
-
-Browser Use provides a number of ways to re-use profiles, sessions, and other configuration across multiple agents.
-
-- ✅ sequential agents can re-use a single `user_data_dir` in new `BrowserSession`s
-- ✅ sequential agents can re-use a single `BrowserSession` without closing it
-- ❌ parallel agents cannot run separate `BrowserSession`s using the same `user_data_dir`
-- ✅ parallel agents can run separate `BrowserSession`s using the same `storage_state`
-- ✅ parallel agents can share a single `BrowserSession`, working in different tabs
-- ⚠️ parallel agents can share a single `BrowserSession`, working in the same tab
-
-
-Multiple `BrowserSession`s (aka chrome processes) cannot share the same `user_data_dir` at the same time, but they can share a `storage_state` file or `BrowserProfile` config.
-
-
-### Sequential Agents, Same Profile, Different Browser
-
-If you are only running one agent & browser at a time, they can re-use the same `user_data_dir` sequentially.
-
-```python
-from browser_use import Agent, BrowserSession
-from langchain_openai import ChatOpenAI
-
-reused_profile = BrowserProfile(user_data_dir='~/.config/browseruse/profiles/default')
-
-agent1 = Agent(
- task="The first task...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_profile=reused_profile, # pass the profile in, it will auto-create a session
-)
-await agent1.run()
-
-agent2 = Agent(
- task="The second task...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_profile=reused_profile, # agent will auto-create its own new session
-)
-await agent2.run()
-```
-
-> Make sure to never mix different browser versions or `executable_path`s with the same `user_data_dir`. Once run with a newer browser version, some migrations are applied to the dir and older browsers wont be able to read it.
-
-### Sequential Agents, Same Profile, Same Browser
-
-If you are only running one agent at a time, they can re-use the same active `BrowserSession` and avoid having to relaunch chrome.
-Each agent will start off looking at the same tab the last agent ended off on.
-
-```python
-from browser_use import Agent, BrowserSession
-from langchain_openai import ChatOpenAI
-
-reused_session = BrowserSession(
- user_data_dir='~/.config/browseruse/profiles/default',
- keep_alive=True, # dont close browser after 1st agent.run() ends
-)
-await reused_session.start() # when keep_alive=True, session must be started manually
-
-agent1 = Agent(
- task="The first task...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_session=reused_session,
-)
-await agent1.run()
-
-agent2 = Agent(
- task="The second task...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_session=reused_session, # re-use the same session
-)
-await agent2.run()
-
-await reused_session.close()
-```
-
-### Parallel Agents, Same Browser, Multiple Tabs
-
-```python
-from browser_use import Agent, BrowserSession
-from langchain_openai import ChatOpenAI
-
-shared_browser = BrowserSession(
- storage_state='/tmp/cookies.json',
- user_data_dir=None,
- keep_alive=True,
- headless=True,
-)
-await shared_browser.start() # when keep_alive=True, you must start the session yourself
-
-agent1 = Agent(
- task="The first task...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_session=shared_browser, # pass the session in
-)
-agent2 = Agent(
- task="The second task...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_session=shared_browser, # re-use the same session
-)
-await asyncio.gather(agent1.run(), agent2.run()) # run in parallel
-
-await shared_browser.close()
-```
-
-### Parallel Agents, Same Browser, Same Tab
-
-
-⚠️ This mode is not recommended. Agents are not yet optimized to share the same tab in the same browser, they may interfere with each other or cause errors.
-
-
-
-```python
-from browser_use import Agent, BrowserSession
-from langchain_openai import ChatOpenAI
-from playwright.async_api import async_playwright
-
-playwright = await async_playwright().start()
-browser = await playwright.chromium.launch(headless=True)
-context = await browser.new_context()
-shared_page = await context.new_page()
-await shared_page.goto('https://example.com', wait_until='domcontentloaded')
-
-shared_session = BrowserSession(page=shared_page, keep_alive=True)
-await shared_session.start()
-
-agent1 = Agent(
- task="Fill out the form in section A...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_session=shared_session
-)
-agent2 = Agent(
- task="Fill out the form in section B...",
- llm=ChatOpenAI(model="gpt-4o-mini"),
- browser_session=shared_session,
-)
-await asyncio.gather(agent1.run(), agent2.run()) # run in parallel
-
-await shared_session.kill()
-```
-
-### Parallel Agents, Same Profile, Different Browsers
-
-
-This mode is the recommended default.
-
-
-To share a single set of configuration or cookies, but still have agents working in their own browser sessions (potentially in parallel), use our provided `BrowserProfile` object.
-
-The recommended way to re-use cookies and localStorage state between separate parallel sessions is to use the [`storage_state`](https://docs.browser-use.com/customize/browser-settings#storage-state) option.
-
-```bash
-# open a browser to log into sites you want the Agent to have access to
-playwright open https://example.com/ --save-storage=/tmp/auth.json
-playwright open https://example.com/ --load-storage=/tmp/auth.json
-```
-
-```python
-from browser_use.browser import BrowserProfile, BrowserSession
-
-shared_profile = BrowserProfile(
- headless=True,
- user_data_dir=None, # use dedicated tmp user_data_dir per session
- storage_state='/tmp/auth.json', # load/save cookies to/from json file
- keep_alive=True, # don't close the browser after the agent finishes
-)
-
-window1 = BrowserSession(browser_profile=profile_a)
-await window1.start()
-agent1 = Agent(browser_session=window1)
-
-window2 = BrowserSession(browser_profile=profile_a)
-await window2.start()
-agent2 = Agent(browser_session=window2)
-
-await asyncio.gather(agent1.run(), agent2.run()) # run in parallel
-await window1.save_storage_state() # write storage state (cookies, localStorage, etc.) to auth.json
-await window2.save_storage_state() # you must decide when to save manually
-
-# can also reload the cookies from the file into the active session if they change
-await window1.load_storage_state()
-await window1.close()
-await window2.close()
-```
-
----
-
-## Troubleshooting
-
-### Chrome Won't Connect
-
-If you're having trouble connecting:
-
-1. **Close all Chrome instances** before trying to launch with a custom profile
-2. **Check if Chrome is running with debugging port**:
- ```bash
- ps aux | grep chrome | grep remote-debugging-port
- ```
-3. **Verify the executable path** is correct for your system
-4. **Check profile permissions** - ensure your user has read/write access
-
-### Profile Lock Issues
-
-If you get a "profile is already in use" error:
-
-1. Close all Chrome instances
-2. The profile will automatically be unlocked when BrowserSession starts
-3. Alternatively, manually delete the `SingletonLock` file in the profile directory
-
-
- For more configuration options, see the [Browser Settings](/customize/browser-settings) documentation.
-
-
-### Profile Version Issues
-
-The browser version you run must always be equal to or greater than the version used to create the `user_data_dir`.
-If you see errors like `Failed to parse Extensions` when launching, you're likely attempting to run an older browser with an incompatible `user_data_dir` that's already been migrated to a newer Chrome version.
-
-Playwright ships a version of chromium that's newer than the default stable Google Chrome release channel, so this can happen if you try to use
-a profile created by the default playwright chromium (e.g. `user_data_dir='~/.config/browseruse/profiles/default'`) with an older
-local browser like `executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'`.
diff --git a/.github/instructions/sensitive-data.instructions.md b/.github/instructions/sensitive-data.instructions.md
deleted file mode 100644
index cf91766..0000000
--- a/.github/instructions/sensitive-data.instructions.md
+++ /dev/null
@@ -1,198 +0,0 @@
----
-description: "Handle sensitive information securely and avoid sending PII & passwords to the LLM."
-applyTo: '**'
----
-
-## Handling Sensitive Data
-
-When working with sensitive information like passwords or PII, you can use the `Agent(sensitive_data=...)` parameter to provide sensitive strings that the model can use in actions without ever seeing directly.
-
-```python
-agent = Agent(
- task='Log into example.com as user x_username with password x_password',
- sensitive_data={
- 'https://example.com': {
- 'x_username': 'abc@example.com',
- 'x_password': 'abc123456', # 'x_placeholder': '',
- },
- },
-)
-```
-
-
-
-You should also configure [`BrowserSession(allowed_domains=...)`](https://docs.browser-use.com/customize/browser-settings#allowed-domains) to prevent the Agent from visiting URLs not needed for the task.
-
-
-
-### Basic Usage
-
-Here's a basic example of how to use sensitive data:
-
-```python
-from dotenv import load_dotenv
-load_dotenv()
-
-from langchain_openai import ChatOpenAI
-from browser_use import Agent, BrowserSession
-
-llm = ChatOpenAI(model='gpt-4o', temperature=0.0)
-
-# Define sensitive data
-# The LLM will only see placeholder names (x_member_number, x_passphrase), never the actual values
-sensitive_data = {
- 'https://*.example.com': {
- 'x_member_number': '123235325',
- 'x_passphrase': 'abcwe234',
- },
-}
-
-# Use the placeholder names in your task description
-task = """
-1. go to https://travel.example.com
-2. sign in with your member number x_member_number and private access code x_passphrase
-3. extract today's list of travel deals as JSON
-"""
-
-# Recommended: Limit the domains available for the entire browser so the Agent can't be tricked into visiting untrusted URLs
-browser_session = BrowserSession(allowed_domains=['https://*.example.com'])
-
-agent = Agent(
- task=task,
- llm=llm,
- sensitive_data=sensitive_data, # Pass the sensitive data to the agent
- browser_session=browser_session, # Pass the restricted browser_session to limit URLs Agent can visit
- use_vision=False, # Disable vision or else the LLM might see entered values in screenshots
-)
-
-async def main():
- await agent.run()
-
-if __name__ == '__main__':
- asyncio.run(main())
-```
-
-In this example:
-
-1. The LLM only ever sees the `x_member_number` and `x_passphrase` placeholders in prompts
-2. When the model wants to use your password it outputs x_passphrase - and we replace it with the actual value in the DOM
-3. When sensitive data appear in the content of the current page, we replace it in the page summary fed to the LLM - so that the model never has it in its state.
-4. The browser will be entirely prevented from going to any site not under `https://*.example.com`
-
-This approach ensures that sensitive information remains secure while still allowing the agent to perform tasks that require authentication.
-
----
-
-### Best Practices
-
-- Always restrict your sensitive data to only the exact domains that need it, `https://travel.example.com` is better than `*.example.com`
-- Always restrict [`BrowserSession(allowed_domains=[...])`](https://docs.browser-use.com/customize/browser-settings#allowed-domains) to only the domains the agent needs to visit to accomplish its task. This helps guard against prompt injection attacks, jailbreaks, and LLM mistakes.
-- Only use `sensitive_data` for strings that can be inputted verbatim as text. The LLM never sees the actual values, so it can't "understand" them, adapt them, or split them up for multiple input fields. For example, you can't ask the Agent to click through a datepicker UI to input the sensitive value `1990-12-31`. For these situations you can implement a [custom function](/customize/custom-functions) the LLM can call that updates the DOM using Python / JS.
-- Don't use `sensitive_data` for login credentials, it's better to use [`storage_state`](docs.browser-use.com/customize/browser-settings#storage-state) or a [`user_data_dir`](/customize/browser-settings#user-data-dir) to log into the sites the agent needs in advance & reuse the cookies:
-
-```bash
-# open a browser to log into the sites you need & save the cookies
-$ playwright open https://accounts.google.com --save-storage auth.json
-```
-
-Then use those cookies when the agent runs:
-
-```python
-agent = Agent(..., browser_session=BrowserSession(storage_state='./auth.json'))
-```
-
-
-
-Warning: Vision models still see the screenshot of the page by default - where the sensitive data might be visible.
-
-It's recommended to set `Agent(use_vision=False)` when working with `sensitive_data`.
-
-
-
-
-
-
-
-
-### Allowed Domains
-
-Domain patterns in `sensitive_data` follow the same format as [`allowed_domains`](https://docs.browser-use.com/customize/browser-settings#allowed-domains):
-
-- `example.com` - Matches only `https://example.com/*`
-- `*.example.com` - Matches `https://example.com/*` and any subdomain `https://*.example.com/*`
-- `http*://example.com` - Matches both `http://` and `https://` protocols for `example.com/*`
-- `chrome-extension://*` - Matches any Chrome extension URL e.g. `chrome-extension://anyextensionid/options.html`
-
-> **Security Warning**: For security reasons, certain patterns are explicitly rejected:
->
-> - Wildcards in TLD part (e.g., `example.*`) are **not allowed** (`google.*` would match `google.ninja`, `google.pizza`, etc. which is a bad idea)
-> - Embedded wildcards (e.g., `g*e.com`) are rejected to prevent overly broad matches
-> - Multiple wildcards like `*.*.domain` are not supported currently, open an issue if you need this feature
-
-The default protocol when no scheme is specified is now `https://` for enhanced security.
-
-For convenience the system will validate that all domain patterns used in `Agent(sensitive_data)` are also included in `BrowserSession(allowed_domains)`.
-
-### Missing or Empty Values
-
-When working with sensitive data, keep these details in mind:
-
-- If a key referenced by the model (`key_name`) is missing from your `sensitive_data` dictionary, a warning will be logged but the substitution tag will be preserved.
-- If you provide an empty value for a key in the `sensitive_data` dictionary, it will be treated the same as a missing key.
-- The system will always attempt to process all valid substitutions, even if some keys are missing or empty.
-
-
----
-
-### Full Example
-
-Here's a more complex example demonstrating multiple domains and sensitive data values.
-
-```python
-from dotenv import load_dotenv
-load_dotenv()
-
-from langchain_openai import ChatOpenAI
-from browser_use import Agent, BrowserSession
-
-
-llm = ChatOpenAI(model='gpt-4o', temperature=0.0)
-
-# Domain-specific sensitive data
-sensitive_data = {
- 'https://*.google.com': {'x_email': '...', 'x_pass': '...'},
- 'chrome-extension://abcd1243': {'x_api_key': '...'},
- 'http*://example.com': {'x_authcode': '123123'}
-}
-
-# Set browser session with allowed domains that match all domain patterns in sensitive_data
-browser_session = BrowserSession(
- allowed_domains=[
- 'https://*.google.com',
- 'chrome-extension://abcd',
- 'http://example.com', # Explicitly include http:// if needed
- 'https://example.com' # By default, only https:// is matched
- ]
-)
-
-# Pass the sensitive data to the agent
-agent = Agent(
- task="Log into Google, then check my account information",
- llm=llm,
- sensitive_data=sensitive_data,
- browser_session=browser_session,
- use_vision=False,
-)
-
-async def main():
- await agent.run()
-
-if __name__ == '__main__':
- asyncio.run(main())
-```
-
-With this approach:
-
-1. The Google credentials (`x_email` and `x_pass`) will only be used on Google domains (any subdomain, https only)
-2. The API key (`x_api_key`) will only be used on pages served by the specific Chrome extension `abcd1243`
-3. The auth code (`x_authcode`) will only be used on `http://example.com/*` or `https://example.com/*`
diff --git a/.github/instructions/supported-models.instructions.md b/.github/instructions/supported-models.instructions.md
deleted file mode 100644
index 333799b..0000000
--- a/.github/instructions/supported-models.instructions.md
+++ /dev/null
@@ -1,294 +0,0 @@
----
-description: "Guide to using different LangChain chat models with Browser Use"
-applyTo: '**'
----
-
-## Overview
-
-Browser Use supports various LangChain chat models. Here's how to configure and use the most popular ones. The full list is available in the [LangChain documentation](https://python.langchain.com/docs/integrations/chat/).
-
-## Model Recommendations
-
-We have yet to test performance across all models. Currently, we achieve the best results using GPT-4o with an 89% accuracy on the [WebVoyager Dataset](https://browser-use.com/posts/sota-technical-report). DeepSeek-V3 is 30 times cheaper than GPT-4o. Gemini-2.0-exp is also gaining popularity in the community because it is currently free.
-We also support local models, like Qwen 2.5, but be aware that small models often return the wrong output structure-which lead to parsing errors. We believe that local models will improve significantly this year.
-
-
-
- All models require their respective API keys. Make sure to set them in your
- environment variables before running the agent.
-
-
-## Supported Models
-
-All LangChain chat models, which support tool-calling are available. We will document the most popular ones here.
-
-### OpenAI
-
-OpenAI's GPT-4o models are recommended for best performance.
-
-```python
-from langchain_openai import ChatOpenAI
-from browser_use import Agent
-
-# Initialize the model
-llm = ChatOpenAI(
- model="gpt-4o",
- temperature=0.0,
-)
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm
-)
-```
-
-Required environment variables:
-
-```bash .env
-OPENAI_API_KEY=
-```
-
-### Anthropic
-
-
-```python
-from langchain_anthropic import ChatAnthropic
-from browser_use import Agent
-
-# Initialize the model
-llm = ChatAnthropic(
- model_name="claude-3-5-sonnet-20240620",
- temperature=0.0,
- timeout=100, # Increase for complex tasks
-)
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm
-)
-```
-
-And add the variable:
-
-```bash .env
-ANTHROPIC_API_KEY=
-```
-
-### Azure OpenAI
-
-```python
-from langchain_openai import AzureChatOpenAI
-from browser_use import Agent
-from pydantic import SecretStr
-import os
-
-# Initialize the model
-llm = AzureChatOpenAI(
- model="gpt-4o",
- api_version='2024-10-21',
- azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
- api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
-)
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm
-)
-```
-
-Required environment variables:
-
-```bash .env
-AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com/
-AZURE_OPENAI_KEY=
-```
-
-
-### Gemini
-
-> [!IMPORTANT]
-> `GEMINI_API_KEY` was the old environment var name, it should be called `GOOGLE_API_KEY` as of 2025-05.
-
-```python
-from langchain_google_genai import ChatGoogleGenerativeAI
-from browser_use import Agent
-from dotenv import load_dotenv
-
-# Read GOOGLE_API_KEY into env
-load_dotenv()
-
-# Initialize the model
-llm = ChatGoogleGenerativeAI(model='gemini-2.0-flash-exp')
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm
-)
-```
-
-Required environment variables:
-
-```bash .env
-GOOGLE_API_KEY=
-```
-
-
-### DeepSeek-V3
-The community likes DeepSeek-V3 for its low price, no rate limits, open-source nature, and good performance.
-The example is available [here](https://github.com/browser-use/browser-use/blob/main/examples/models/deepseek.py).
-
-```python
-from langchain_deepseek import ChatDeepSeek
-from browser_use import Agent
-from pydantic import SecretStr
-from dotenv import load_dotenv
-import os
-
-load_dotenv()
-api_key = os.getenv("DEEPSEEK_API_KEY")
-
-# Initialize the model
-llm=ChatDeepSeek(base_url='https://api.deepseek.com/v1', model='deepseek-chat', api_key=SecretStr(api_key))
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm,
- use_vision=False
-)
-```
-
-Required environment variables:
-
-```bash .env
-DEEPSEEK_API_KEY=
-```
-
-### DeepSeek-R1
-We support DeepSeek-R1. Its not fully tested yet, more and more functionality will be added, like e.g. the output of it'sreasoning content.
-The example is available [here](https://github.com/browser-use/browser-use/blob/main/examples/models/deepseek-r1.py).
-It does not support vision. The model is open-source so you could also use it with Ollama, but we have not tested it.
-```python
-from langchain_deepseek import ChatDeepSeek
-from browser_use import Agent
-from pydantic import SecretStr
-from dotenv import load_dotenv
-import os
-
-load_dotenv()
-api_key = os.getenv("DEEPSEEK_API_KEY")
-
-# Initialize the model
-llm=ChatDeepSeek(base_url='https://api.deepseek.com/v1', model='deepseek-reasoner', api_key=SecretStr(api_key))
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm,
- use_vision=False
-)
-```
-
-Required environment variables:
-
-```bash .env
-DEEPSEEK_API_KEY=
-```
-
-### Ollama
-Many users asked for local models. Here they are.
-
-1. Download Ollama from [here](https://ollama.ai/download)
-2. Run `ollama pull model_name`. Pick a model which supports tool-calling from [here](https://ollama.com/search?c=tools)
-3. Run `ollama start`
-
-```python
-from langchain_ollama import ChatOllama
-from browser_use import Agent
-from pydantic import SecretStr
-
-
-# Initialize the model
-llm=ChatOllama(model="qwen2.5", num_ctx=32000)
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm
-)
-```
-
-Required environment variables: None!
-
-### Novita AI
-[Novita AI](https://novita.ai) is an LLM API provider that offers a wide range of models. Note: choose a model that supports function calling.
-
-```python
-from langchain_openai import ChatOpenAI
-from browser_use import Agent
-from pydantic import SecretStr
-from dotenv import load_dotenv
-import os
-
-load_dotenv()
-api_key = os.getenv("NOVITA_API_KEY")
-
-# Initialize the model
-llm = ChatOpenAI(base_url='https://api.novita.ai/v3/openai', model='deepseek/deepseek-v3-0324', api_key=SecretStr(api_key))
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm,
- use_vision=False
-)
-```
-
-Required environment variables:
-
-```bash .env
-NOVITA_API_KEY=
-```
-### X AI
-[X AI](https://x.ai) is an LLM API provider that offers a wide range of models. Note: choose a model that supports function calling.
-
-```python
-from langchain_openai import ChatOpenAI
-from browser_use import Agent
-from pydantic import SecretStr
-from dotenv import load_dotenv
-import os
-
-load_dotenv()
-api_key = os.getenv("GROK_API_KEY")
-
-# Initialize the model
-llm = ChatOpenAI(
- base_url='https://api.x.ai/v1',
- model='grok-3-beta',
- api_key=SecretStr(api_key)
-)
-
-# Create agent with the model
-agent = Agent(
- task="Your task here",
- llm=llm,
- use_vision=False
-)
-```
-
-Required environment variables:
-
-```bash .env
-GROK_API_KEY=
-```
-
-## Coming soon
-(We are working on it)
-- Groq
-- Github
-- Fine-tuned models
diff --git a/.github/instructions/system-prompt.instructions.md b/.github/instructions/system-prompt.instructions.md
deleted file mode 100644
index 65da9a1..0000000
--- a/.github/instructions/system-prompt.instructions.md
+++ /dev/null
@@ -1,76 +0,0 @@
----
-description: "Customize the system prompt to control agent behavior and capabilities"
-applyTo: '**'
----
-
-## Overview
-
-You can customize the system prompt in two ways:
-
-1. Extend the default system prompt with additional instructions
-2. Override the default system prompt entirely
-
-
- Custom system prompts allow you to modify the agent's behavior at a
- fundamental level. Use this feature carefully as it can significantly impact
- the agent's performance and reliability.
-
-
-### Extend System Prompt (recommended)
-
-To add additional instructions to the default system prompt:
-
-```python
-extend_system_message = """
-REMEMBER the most important RULE:
-ALWAYS open first a new tab and go first to url wikipedia.com no matter the task!!!
-"""
-```
-
-### Override System Prompt
-
-
- Not recommended! If you must override the [default system
- prompt](https://github.com/browser-use/browser-use/blob/main/browser_use/agent/system_prompt.md),
- make sure to test the agent yourself.
-
-
-Anyway, to override the default system prompt:
-
-```python
-# Define your complete custom prompt
-override_system_message = """
-You are an AI agent that helps users with web browsing tasks.
-
-[Your complete custom instructions here...]
-"""
-
-# Create agent with custom system prompt
-agent = Agent(
- task="Your task here",
- llm=ChatOpenAI(model='gpt-4'),
- override_system_message=override_system_message
-)
-```
-
-### Extend Planner System Prompt
-
-You can customize the behavior of the planning agent by extending its system prompt:
-
-```python
-extend_planner_system_message = """
-PRIORITIZE gathering information before taking any action.
-Always suggest exploring multiple options before making a decision.
-"""
-
-# Create agent with extended planner system prompt
-llm = ChatOpenAI(model='gpt-4o')
-planner_llm = ChatOpenAI(model='gpt-4o-mini')
-
-agent = Agent(
- task="Your task here",
- llm=llm,
- planner_llm=planner_llm,
- extend_planner_system_message=extend_planner_system_message
-)
-```
diff --git a/.legacy/run.ps1 b/.legacy/run.ps1
deleted file mode 100644
index 31c3670..0000000
--- a/.legacy/run.ps1
+++ /dev/null
@@ -1,36 +0,0 @@
-# ── 설정 부분 ──
-# 실행할 Python 스크립트 이름 (파일 확장자까지)
-$PYTHON_SCRIPT = "main.py"
-
-# 도메인 목록 파일 경로 (Python 스크립트 실행 시 -f 옵션에 전달)
-$DOMAIN_FILE = "./data/domains.txt"
-# ─────────────
-
-# https://f.imnya.ng/.whs/tp-domains/data/domains/latest.txt
-# domains.txt 파일을 다운로드하는 명령어
-
-curl "https://f.imnya.ng/.whs/tp-domains/data/domains/latest.txt" -o $DOMAIN_FILE
-
-# 인자 개수 확인 (2개 또는 3개)
-if ($args.Count -lt 2 -or $args.Count -gt 3) {
- Write-Host "Usage: $($MyInvocation.MyCommand.Name) [skip_header]"
- Write-Host "예시) $($MyInvocation.MyCommand.Name) 10000 11000"
- Write-Host "예시) $($MyInvocation.MyCommand.Name) 10000 11000 True"
- exit 1
-}
-
-$START_LINE = [int]$args[0]
-$END_LINE = [int]$args[1]
-$SKIP_HEADER = if ($args.Count -eq 3) { $args[2] } else { "False" }
-
-$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
-Write-Host "[$timestamp] Processing lines $START_LINE to $END_LINE..."
-
-# Python 스크립트 실행
-# -f DOMAIN_FILE: 도메인 목록 파일 경로
-# -s START_LINE : 읽기 시작 줄
-# -e END_LINE : 읽기 끝 줄
-# -skh SKIP_HEADER: 헤더 스킵 여부
-uv run $PYTHON_SCRIPT -f $DOMAIN_FILE -s $START_LINE -e $END_LINE -skh $SKIP_HEADER
-
-Write-Host "처리 완료."
diff --git a/.legacy/run.sh b/.legacy/run.sh
deleted file mode 100644
index 6777588..0000000
--- a/.legacy/run.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/bash
-
-# ── 설정 부분 ──
-PYTHON_SCRIPT="main.py"
-DOMAIN_FILE="./data/domains.txt"
-# ─────────────
-
-curl "https://f.imnya.ng/.whs/tp-domains/data/domains/latest.txt" -o $DOMAIN_FILE
-
-# 인자 개수 확인
-if [ $# -lt 2 ]; then
- echo "Usage: $0 [skh_option]"
- echo "예시) $0 10000 11000 True"
- exit 1
-fi
-
-START_LINE=$1
-END_LINE=$2
-SKH_OPTION=$3
-
-if [ -z "$SKH_OPTION" ]; then
- SKH_OPTION="False"
-fi
-
-echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing lines ${START_LINE} to ${END_LINE}..."
-uv run "$PYTHON_SCRIPT" -f "$DOMAIN_FILE" -s "$START_LINE" -e "$END_LINE" -skh $SKH_OPTION
-
-echo "처리 완료."
diff --git a/README.md b/README.md
index 638e9f2..d34e542 100644
--- a/README.md
+++ b/README.md
@@ -34,10 +34,6 @@
uv run setup.py
```
-
-
-설치 및 설정 (레거시)
-
uv 설치 후 다음과 같은 명령어를 입력합니다.
```sh
@@ -48,45 +44,7 @@ venv와 패키지가 설치가 됩니다.
---
-~~browser_use가 Playwright에 대한 의존성이 있어 브라우저 설치가 필요합니다~~
-
-스텔스 기능 때문에 Google Chrome이 필요합니다.
-
-만약 설치가 되어 있지 않다면
-```
-playwright install chrome
-```
-
-Environment는 .env.example에 따라 설정되어야합니다.
-
-.env.example을 .env로 복사하여서 사용해주세요.
-
-## 로그인 방안
-
-### 쿠키와 로컬 스토리지 설정 방법 (추천)
-
-
-
-```sh
-uv run playwright open https://google.com/ --save-storage=./data/storage_state.json
-```
-
-위 명령어를 실행하면 playwright Browser가 하나 열리는데 여기서 원하는 프로바이더를 모두 로그인 한 후에 브라우저를 정상적으로 닫으면 ./data/storage_state.json 경로에 쿠키, 로컬스토리지를 저장한 파일이 생성됩니다.
-
-### Browser Use에게 직접 로그인 요청 (선택)
-
-위에 쿠키와 로컬스토리지 설정 방법과 혼용해서 사용가능합니다.
-
-`.sensitive.example.json`을 `.sensitive.json`으로 복사해서
-
-안에 있는 예시 내용을 참고해서 작성해주시면 됩니다.
-더 자세한 내용은
-[Sensitive Data - Browser Use](https://docs.browser-use.com/customize/sensitive-data)를 참고하시면 좋을 것 같습니다.
-
-[Sensitive Data - Browser Use](https://docs.browser-use.com/customize/sensitive-data)에서도 권장하지 않는 방법인만큼 애매하긴 하지만 쿠키와 로컬 스토리지를 저장하기 어려운 경우나 일부 flow에서 접근이 어려운 경우 사용해주세요.
-
-
-
+`uv run setup.py`로 환경을 설정합니다.
---
@@ -94,7 +52,8 @@ uv run playwright open https://google.com/ --save-storage=./data/storage_state.j
이거 해결 방법

-
+
+
이것도 setup.py 사용하면 반자동으로 할 수 있습니다.
못찾겠으면 intl.cpl 열어주세요.
@@ -132,7 +91,7 @@ Prompt에서 추가한 파일을 prompt.py에서 수정합니다.
응답할 때 원하는 리턴 값을 `dict`로 받습니다.
## 4. \_\_init\_\_.py 수정
-
+
추가한 prompt에 따라 import합니다.
diff --git a/docs/encode.png b/docs/encode.png
deleted file mode 100644
index 5eb0a20..0000000
Binary files a/docs/encode.png and /dev/null differ
diff --git a/docs/guide_0.png b/docs/guide_0.png
deleted file mode 100644
index 9dc3f75..0000000
Binary files a/docs/guide_0.png and /dev/null differ
diff --git a/docs/image.png b/docs/image.png
deleted file mode 100644
index b29e336..0000000
Binary files a/docs/image.png and /dev/null differ
diff --git a/docs/list.png b/docs/list.png
deleted file mode 100644
index 1a005de..0000000
Binary files a/docs/list.png and /dev/null differ
diff --git a/pyproject.toml b/pyproject.toml
index 522ed65..ec5b8c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,8 +5,10 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
+ "black>=25.1.0",
"browser-use[memory]==0.3.3",
"chardet>=5.2.0",
+ "isort>=6.0.1",
"lmnr[all]>=0.6.10",
"patchright>=1.52.5",
]
diff --git a/run.py b/run.py
index e3b27b4..bc1afca 100644
--- a/run.py
+++ b/run.py
@@ -1,9 +1,10 @@
-import sys
-import subprocess
-import os
-import requests
-from datetime import datetime
import argparse
+import os
+import subprocess
+import sys
+from datetime import datetime
+
+import requests
#!/usr/bin/env python3
diff --git a/setup.py b/setup.py
index 551d91b..f4989fb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,12 @@
import os
import subprocess
import webbrowser
+import asyncio
+from browser_use import BrowserProfile, Agent
+from browser_use.llm import ChatGoogle
+from dotenv import load_dotenv
+import threading
+load_dotenv(verbose=True, override=True)
os.makedirs(os.path.dirname("./data"), exist_ok=True)
@@ -77,20 +83,64 @@ def i_dont_like_windows():
input("계속하려면 Enter 키를 누르세요...")
-def setup_storage():
- print("\n🔧 쿠키와 로컬 스토리지를 설정하시겠습니까?")
- print("👀 다음 단계에서 Senstive Data를 설정할 수 있지만 쿠키와 로컬 스토리지를 더 권장합니다.")
+async def setup_user_data():
+ print("\n📂 사용자 데이터 디렉토리를 설정하시겠습니까?")
+ print("⚠️ 사용자 데이터 디렉토리는 브라우저의 프로필 데이터를 저장하는 곳입니다.")
+ print("✅ 이 작업은 Google API Key를 설정하고 나서 진행해야만합니다.")
if prompt_yes_no("\033[1m\033[33m선택하시려면 y를 입력하세요 (y/n):\033[0m "):
+ if os.getenv("GOOGLE_API_KEY") is None:
+ print("⚠️ Google API Key가 설정되어 있지 않습니다. 먼저 Google API Key를 설정해주세요.")
+ return
print("======================================================")
- print("👀 원하는 OAuth Providor를 직접 모두 로그인 한 후에 브라우저를 닫으면 설정이 완료됩니다.")
- os.system('uv run playwright open https://google.com/ --save-storage=./data/storage_state.json')
- print("✅ 쿠키와 로컬 스토리지 설정 완료.")
- print("💾 ./data/storage_state.json 파일이 생성되었습니다.")
- else:
- print("🚫 쿠키와 로컬 스토리지 설정이 취소되었습니다.")
- print("======================================================")
- print("⚠️ 이후에 쿠키와 로컬 스토리지를 설정하려면, `uv run playwright open https://google.com/ --save-storage=./data/storage_state.json` 명령어를 사용하세요.\n")
+ llm = ChatGoogle(
+ model="gemini-2.0-flash",
+ )
+ initial_actions = [
+ {'go_to_url': {'url': 'https://www.google.com', 'new_tab': False}},
+ {'wait': {'seconds': 2147483647}},
+ ]
+ agent = Agent(
+ task="Just Wait",
+ llm=llm,
+ use_vision=False,
+ initial_actions=initial_actions,
+ browser_profile=BrowserProfile(
+ disable_security=True,
+ stealth=True,
+ headless=False,
+ device_scale_factor=1,
+ window_size={"width": 1600, "height": 900},
+ viewport={"width": 1600, "height": 900},
+ user_data_dir="./data/user_data",
+ )
+ )
+
+ print("======================================================\n")
+ print("👉 브라우저가 열립니다. 필요한 로그인을 완료한 후 엔터키를 눌러 다음 단계로 진행하세요.")
+ input("계속하려면 Enter 키를 누르세요...\n")
+ print("======================================================")
+
+ # 브라우저를 백그라운드에서 시작
+ def run_agent():
+ asyncio.run(agent.run())
+
+ agent_thread = threading.Thread(target=run_agent)
+ agent_thread.daemon = True
+ agent_thread.start()
+
+ # 사용자가 'n'을 입력할 때까지 대기
+ while True:
+ user_input = input("").strip().lower()
+ if user_input == '':
+ break
+
+ print("======================================================")
+ print("✅ 설정이 완료되었습니다.")
+ else:
+ print("🚫 설정이 취소되었습니다.")
+ print("======================================================")
+ print("⚠️ 이후에 USER_DATA_DIR을 설정하려면, .env 파일을 참고하여 USER_DATA_DIR을 설정하세요.\n")
def setup_sensitive():
print("\n🔐 Sensitive Data을 설정하시겠습니까?")
@@ -109,7 +159,6 @@ def setup_sensitive():
print("======================================================")
print("⚠️ 이후에 민감 정보 파일을 설정하려면, .sensitive.example.json 파일을 참고하여 .sensitive.json 파일을 생성하세요.\n")
-
if __name__ == "__main__":
# 1. .env 생성
create_file_from_example('.env', '.env.example')
@@ -122,11 +171,12 @@ if __name__ == "__main__":
i_dont_like_windows()
print("=====================================================")
- # 4. 쿠키와 로컬 스토리지 설정
- setup_storage()
+ # 4. Setup User Data
+ asyncio.run(setup_user_data())
print("=====================================================")
# 5. .sensitive.json 생성
# setup_sensitive()
print("=====================================================")
print("🎉 초기 설정이 완료되었습니다! 이제 스크립트를 실행할 준비가 되었습니다.")
+ print("🎉 초기 설정이 완료되었습니다! 이제 스크립트를 실행할 준비가 되었습니다.")
diff --git a/src/lib/browser_use/__init__.py b/src/lib/browser_use/__init__.py
index 6814f2a..3d47e31 100644
--- a/src/lib/browser_use/__init__.py
+++ b/src/lib/browser_use/__init__.py
@@ -1,7 +1,7 @@
+from lib.browser_use.agents import *
from lib.browser_use.clean_resources import *
from lib.browser_use.func import *
-from lib.browser_use.model import *
from lib.browser_use.init_profile import *
+from lib.browser_use.model import *
+from lib.browser_use.scanner import *
from lib.browser_use.sensitive_data import *
-from lib.browser_use.agents import *
-from lib.browser_use.scanner import *
\ No newline at end of file
diff --git a/src/lib/browser_use/agents.py b/src/lib/browser_use/agents.py
index af424e4..d03a6b4 100644
--- a/src/lib/browser_use/agents.py
+++ b/src/lib/browser_use/agents.py
@@ -1,31 +1,28 @@
import asyncio
-import os
import json
-from typing import Dict, Any, Optional
+import os
from dataclasses import dataclass
from datetime import datetime, timedelta
+from typing import Any, Dict, Optional
from browser_use import Agent, BrowserSession, Controller
from patchright.async_api import async_playwright as async_patchright
-from lib.browser_use import (
- GetProfile,
- GetSensitiveData,
- clean_resources,
-)
-from lib.utils import (
- logger,
- config,
-)
+from lib.browser_use.clean_resources import clean_resources
+from lib.browser_use.init_profile import GetProfile
+from lib.browser_use.sensitive_data import GetSensitiveData
from lib.llm import CreateChatGoogle, get_prompt
+from lib.utils import config, logger
# Exponential backoff settings
INITIAL_BACKOFF = int(os.getenv("INITIAL_BACKOFF", "60")) # seconds
MAX_BACKOFF = int(os.getenv("MAX_BACKOFF", "600")) # seconds
+
@dataclass
class RetryTask:
"""재시도할 작업을 나타내는 클래스"""
+
task_type: str # "oauth_list" or "oauth_login"
url: str
oauth_provider: Optional[str] = None
@@ -33,46 +30,55 @@ class RetryTask:
next_retry_time: Optional[datetime] = None
max_retries: int = 5
+
# 전역 재시도 큐
retry_queue: list[RetryTask] = []
retry_queue_lock = asyncio.Lock()
+
async def add_to_retry_queue(task: RetryTask):
"""작업을 재시도 큐에 추가"""
async with retry_queue_lock:
# 중복 작업 확인
existing_task = None
for existing in retry_queue:
- if (existing.task_type == task.task_type and
- existing.url == task.url and
- existing.oauth_provider == task.oauth_provider):
+ if (
+ existing.task_type == task.task_type
+ and existing.url == task.url
+ and existing.oauth_provider == task.oauth_provider
+ ):
existing_task = existing
break
-
+
if existing_task:
# 기존 작업이 있으면 재시도 횟수 업데이트
existing_task.retry_count = task.retry_count
existing_task.next_retry_time = task.next_retry_time
- print(f"📝 기존 작업 업데이트: {task.task_type} - {task.url} (재시도: {task.retry_count})")
+ print(
+ f"📝 기존 작업 업데이트: {task.task_type} - {task.url} (재시도: {task.retry_count})"
+ )
else:
# 새 작업 추가
retry_queue.append(task)
- print(f"➕ 재시도 큐에 작업 추가: {task.task_type} - {task.url} (재시도: {task.retry_count})")
+ print(
+ f"➕ 재시도 큐에 작업 추가: {task.task_type} - {task.url} (재시도: {task.retry_count})"
+ )
+
async def process_retry_queue():
"""재시도 큐 처리"""
async with retry_queue_lock:
now = datetime.now()
ready_tasks = []
-
+
for task in retry_queue[:]: # 복사본에서 반복
if task.next_retry_time and task.next_retry_time <= now:
ready_tasks.append(task)
retry_queue.remove(task)
-
+
if ready_tasks:
print(f"🔄 {len(ready_tasks)}개의 재시도 작업 처리 중...")
-
+
for task in ready_tasks:
try:
if task.task_type == "oauth_list":
@@ -82,20 +88,25 @@ async def process_retry_queue():
else:
await _handle_retry_failure(task)
elif task.task_type == "oauth_login":
- result = await _test_oauth_login_internal(task.url, task.oauth_provider)
+ result = await _test_oauth_login_internal(
+ task.url, task.oauth_provider
+ )
if result:
- print(f"✅ 재시도 성공: {task.oauth_provider} 로그인 - {task.url}")
+ print(
+ f"✅ 재시도 성공: {task.oauth_provider} 로그인 - {task.url}"
+ )
else:
await _handle_retry_failure(task)
except Exception as e:
print(f"❌ 재시도 중 에러: {e}")
await _handle_retry_failure(task)
+
async def _handle_retry_failure(task: RetryTask):
"""재시도 실패 처리"""
if task.retry_count < task.max_retries:
task.retry_count += 1
- wait_time = min(INITIAL_BACKOFF * (2 ** task.retry_count), MAX_BACKOFF)
+ wait_time = min(INITIAL_BACKOFF * (2**task.retry_count), MAX_BACKOFF)
task.next_retry_time = datetime.now() + timedelta(seconds=wait_time)
await add_to_retry_queue(task)
print(f"⏰ {wait_time}초 후 재시도 예정: {task.task_type} - {task.url}")
@@ -103,6 +114,7 @@ async def _handle_retry_failure(task: RetryTask):
print(f"❌ 최대 재시도 횟수 초과: {task.task_type} - {task.url}")
logger(f"❌ 최대 재시도 횟수 초과: {task.task_type} - {task.url}")
+
async def get_retry_queue_status():
"""재시도 큐 상태 조회"""
async with retry_queue_lock:
@@ -114,45 +126,56 @@ async def get_retry_queue_status():
"url": task.url,
"oauth_provider": task.oauth_provider,
"retry_count": task.retry_count,
- "next_retry_time": task.next_retry_time.isoformat() if task.next_retry_time else None
+ "next_retry_time": (
+ task.next_retry_time.isoformat()
+ if task.next_retry_time
+ else None
+ ),
}
for task in retry_queue
- ]
+ ],
}
+
async def _run_agent_with_retry(agent_config):
"""Agent 실행을 위한 내부 헬퍼 함수 (재시도 로직 포함)"""
agent = None
session = None
try_cnt = 0
url = agent_config["url"]
-
+ headless = os.getenv("HEADLESS", "False").lower() == "true"
+
while try_cnt < 3:
try:
session = BrowserSession(
playwright=(await async_patchright().start()),
- browser_profile=await GetProfile(),
+ browser_profile=await GetProfile(headless=headless),
)
- agent = Agent(
- browser_session=session,
- **agent_config["agent_params"]
- )
+ agent = Agent(browser_session=session, **agent_config["agent_params"])
response = await agent.run()
await clean_resources(agent, session)
- if any(keyword in str(response) for keyword in [
- "429", "resource_exhausted", "resourceexhausted",
- "quota", "rate limit", "too many requests",
- "exceeded", "limit reached"
- ]):
+ if any(
+ keyword in str(response)
+ for keyword in [
+ "429",
+ "resource_exhausted",
+ "resourceexhausted",
+ "quota",
+ "rate limit",
+ "too many requests",
+ "exceeded",
+ "limit reached",
+ ]
+ ):
print(f"⚠️ API 쿼터 에러 발생, 재시도 큐에 추가: {url}")
task = RetryTask(
task_type=agent_config.get("task_type", "unknown"),
url=url,
retry_count=try_cnt + 1,
- next_retry_time=datetime.now() + timedelta(seconds=INITIAL_BACKOFF)
+ next_retry_time=datetime.now() + timedelta(seconds=INITIAL_BACKOFF),
)
await add_to_retry_queue(task)
return None
@@ -166,10 +189,12 @@ async def _run_agent_with_retry(agent_config):
try_cnt += 1
if try_cnt >= 3:
error_msg = f"최대 재시도 횟수 초과."
- logger(f"❌ {url} - {agent_config['log_context']} 실패: {error_msg}: {e}")
+ logger(
+ f"❌ {url} - {agent_config['log_context']} 실패: {error_msg}: {e}"
+ )
print(f"❌ {url} - {agent_config['log_context']} 실패: {error_msg}")
return None
-
+
print(f"⚠️ 에러 발생: {e}. {try_cnt}번째 재시도 중...")
await asyncio.sleep(30)
continue
@@ -186,7 +211,7 @@ async def _extract_oauth_list_internal(url: str):
"url": target_url,
"log_context": "OAuth 리스트 추출",
"agent_params": {
- "initial_actions": [{"open_tab": {"url": target_url}}],
+ "initial_actions": [{"go_to_url": {"url": target_url, 'new_tab': False}}],
"sensitive_data": GetSensitiveData(),
"task": (
"Navigate to the login page and identify all OAuth provider buttons (excluding Passkey). "
@@ -197,7 +222,8 @@ async def _extract_oauth_list_internal(url: str):
"llm": CreateChatGoogle(config.GOOGLE_MODEL),
"planner_llm": (
CreateChatGoogle(config.GOOGLE_PLANNER_MODEL)
- if config.GOOGLE_PLANNER_MODEL and os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LIST")
+ if config.GOOGLE_PLANNER_MODEL
+ and os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LIST")
else None
),
"controller": Controller(
@@ -206,7 +232,7 @@ async def _extract_oauth_list_internal(url: str):
),
"extend_system_message": prompt,
"extend_planner_system_message": prompt,
- }
+ },
}
response = await _run_agent_with_retry(agent_config)
@@ -241,17 +267,25 @@ async def extract_oauth_list(url: str):
return await _extract_oauth_list_internal(url)
except Exception as e:
error_str = str(e).lower()
- if any(keyword in error_str for keyword in [
- "429", "resource_exhausted", "resourceexhausted",
- "quota", "rate limit", "too many requests",
- "exceeded", "limit reached"
- ]):
+ if any(
+ keyword in error_str
+ for keyword in [
+ "429",
+ "resource_exhausted",
+ "resourceexhausted",
+ "quota",
+ "rate limit",
+ "too many requests",
+ "exceeded",
+ "limit reached",
+ ]
+ ):
print(f"⚠️ API 쿼터 에러 발생, 재시도 큐에 추가: {url}")
task = RetryTask(
task_type="oauth_list",
url=url,
retry_count=1,
- next_retry_time=datetime.now() + timedelta(seconds=INITIAL_BACKOFF)
+ next_retry_time=datetime.now() + timedelta(seconds=INITIAL_BACKOFF),
)
await add_to_retry_queue(task)
return []
@@ -270,7 +304,7 @@ async def _test_oauth_login_internal(url: str, oauth_provider: str):
"url": target_url,
"log_context": f"{oauth_provider} 로그인",
"agent_params": {
- "initial_actions": [{"open_tab": {"url": target_url}}],
+ "initial_actions": [{"go_to_url": {"url": target_url, 'new_tab': False}}],
"sensitive_data": GetSensitiveData(),
"task": (
f"Navigate to the login page, find and click the {oauth_provider} OAuth button, "
@@ -282,7 +316,8 @@ async def _test_oauth_login_internal(url: str, oauth_provider: str):
"llm": CreateChatGoogle(config.GOOGLE_MODEL),
"planner_llm": (
CreateChatGoogle(config.GOOGLE_PLANNER_MODEL)
- if config.GOOGLE_PLANNER_MODEL and os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LOGIN")
+ if config.GOOGLE_PLANNER_MODEL
+ and os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LOGIN")
else None
),
"controller": Controller(
@@ -291,7 +326,7 @@ async def _test_oauth_login_internal(url: str, oauth_provider: str):
),
"extend_system_message": prompt,
"extend_planner_system_message": prompt,
- }
+ },
}
response = await _run_agent_with_retry(agent_config)
@@ -301,7 +336,7 @@ async def _test_oauth_login_internal(url: str, oauth_provider: str):
print(f"✅ {oauth_provider} 로그인 완료")
logger(f"✅ {url} - {oauth_provider} 로그인 결과: {final_result}")
return True
-
+
print(f"❌ {oauth_provider} 로그인 실패")
return False
@@ -312,26 +347,36 @@ async def test_oauth_login(url: str, oauth_provider: str):
return await _test_oauth_login_internal(url, oauth_provider)
except Exception as e:
error_str = str(e).lower()
- if any(keyword in error_str for keyword in [
- "429", "resource_exhausted", "resourceexhausted",
- "quota", "rate limit", "too many requests",
- "exceeded", "limit reached"
- ]):
+ if any(
+ keyword in error_str
+ for keyword in [
+ "429",
+ "resource_exhausted",
+ "resourceexhausted",
+ "quota",
+ "rate limit",
+ "too many requests",
+ "exceeded",
+ "limit reached",
+ ]
+ ):
print(f"⚠️ API 쿼터 에러 발생, 재시도 큐에 추가: {oauth_provider} - {url}")
task = RetryTask(
task_type="oauth_login",
url=url,
oauth_provider=oauth_provider,
retry_count=1,
- next_retry_time=datetime.now() + timedelta(seconds=INITIAL_BACKOFF)
+ next_retry_time=datetime.now() + timedelta(seconds=INITIAL_BACKOFF),
)
await add_to_retry_queue(task)
return False
else:
raise e
+
async def start_retry_queue_processor():
"""재시도 큐 처리기를 백그라운드에서 시작"""
+
async def queue_processor():
while True:
try:
@@ -340,14 +385,15 @@ async def start_retry_queue_processor():
except Exception as e:
print(f"❌ 재시도 큐 처리 중 에러: {e}")
await asyncio.sleep(60) # 에러 발생 시 1분 대기
-
+
# 백그라운드 태스크로 실행
asyncio.create_task(queue_processor())
print("🔄 재시도 큐 처리기 시작됨")
+
# 모듈 로딩 시 자동으로 백그라운드 처리기 시작
# (실제 애플리케이션에서는 main 함수에서 호출하는 것이 좋음)
def init_retry_system():
"""재시도 시스템 초기화"""
print("🔧 재시도 시스템 초기화 중...")
- # 이 함수는 메인 애플리케이션에서 호출해야 함
\ No newline at end of file
+ # 이 함수는 메인 애플리케이션에서 호출해야 함
diff --git a/src/lib/browser_use/clean_resources.py b/src/lib/browser_use/clean_resources.py
index 792be35..6044e03 100644
--- a/src/lib/browser_use/clean_resources.py
+++ b/src/lib/browser_use/clean_resources.py
@@ -1,5 +1,6 @@
from pathlib import Path
+
async def clean_resources(agent=None, session=None):
"""리소스를 정리하는 함수"""
storage_state_temp_path = Path("./data/storage_state_temp.json").resolve()
diff --git a/src/lib/browser_use/func.py b/src/lib/browser_use/func.py
index 5c2faa8..0d66fb5 100644
--- a/src/lib/browser_use/func.py
+++ b/src/lib/browser_use/func.py
@@ -1,47 +1,13 @@
-import os
import json
+import os
from pathlib import Path
-from dotenv import load_dotenv
+
from browser_use import BrowserProfile
-import json
-import os
+from dotenv import load_dotenv
# Load environment variables
load_dotenv(override=True)
-async def setup_storage_state():
- """Setup browser storage state for session persistence."""
- # Get the script directory to ensure correct path resolution
- script_dir = Path(__file__).parent.parent.parent.parent
- storage_state_path = script_dir / "data" / "storage_state.json"
- storage_state_temp_path = script_dir / "data" / "storage_state_temp.json"
-
- print(f"📂 Storage state path: {storage_state_path}")
- print(f"📂 Temp storage state path: {storage_state_temp_path}")
-
- if storage_state_path.exists():
- try:
- if storage_state_temp_path.exists():
- storage_state_temp_path.unlink()
-
- with open(storage_state_path, 'r') as f:
- storage_data = json.load(f)
-
- with open(storage_state_temp_path, 'w') as f:
- json.dump(storage_data, f, indent=4)
-
- print(f"🔄 Using existing storage state: {storage_state_temp_path}")
- return str(storage_state_temp_path)
-
- except Exception as e:
- print(f"⚠️ Error processing storage state: {e}")
- if storage_state_temp_path.exists():
- storage_state_temp_path.unlink()
- return None
-
- print("⚠️ No existing storage state found")
- return None
-
def setup_proxy():
"""Configure proxy settings from environment variables."""
diff --git a/src/lib/browser_use/init_profile.py b/src/lib/browser_use/init_profile.py
index dd3f3b8..bfc38df 100644
--- a/src/lib/browser_use/init_profile.py
+++ b/src/lib/browser_use/init_profile.py
@@ -1,44 +1,39 @@
import os
+import shutil
+import tempfile
+
from lib.browser_use.func import *
+from lib.utils.config import USER_DATA_DIR
# Initialize configuration
proxy_url = setup_proxy()
-async def GetProfile():
- storage_state_path = await setup_storage_state()
-
- # Handle potential encoding issues with storage state file
- try:
- if storage_state_path and os.path.exists(storage_state_path):
- # Test if file can be read properly, if not, skip it
- with open(storage_state_path, 'r', encoding='utf-8') as f:
- f.read()
- storage_state = storage_state_path
- else:
- print("⚠️ Storage state file not found or inaccessible, proceeding without it.")
- storage_state = None
- except (UnicodeDecodeError, FileNotFoundError):
- # If there's an encoding error, don't use the storage state
- storage_state = None
-
+
+async def GetProfile(headless=False):
+ user_data_dir = None
+ if USER_DATA_DIR and os.path.isdir(USER_DATA_DIR):
+ try:
+ tmp_user_data_dir = tempfile.mkdtemp()
+ shutil.copytree(USER_DATA_DIR, tmp_user_data_dir, dirs_exist_ok=True)
+ user_data_dir = tmp_user_data_dir
+ print(f"✅ Copied user data dir to temporary location: {user_data_dir}")
+ except Exception as e:
+ print(f"❌ Failed to copy user data dir: {e}")
+
profile = BrowserProfile(
# Security settings
disable_security=True,
stealth=True,
-
# Display settings
- headless=False,
+ headless=headless,
device_scale_factor=1,
window_size={"width": 1600, "height": 900},
viewport={"width": 1600, "height": 900},
-
# Data persistence
- user_data_dir=None,
- storage_state=storage_state,
-
+ user_data_dir=user_data_dir,
+ #storage_state=storage_state,
# Network settings
proxy={"server": proxy_url} if proxy_url else None,
-
# Additional arguments
args=get_browser_args(),
)
diff --git a/src/lib/browser_use/model.py b/src/lib/browser_use/model.py
index 6a1178f..b52d650 100644
--- a/src/lib/browser_use/model.py
+++ b/src/lib/browser_use/model.py
@@ -1,6 +1,8 @@
from typing import List
+
from pydantic import BaseModel
+
# 출력 모델
class OAuth(BaseModel):
provider: str
@@ -12,4 +14,4 @@ class OAuthList(BaseModel):
# 기존 모델 유지 (backward compatibility)
-BaseModel = OAuthList
\ No newline at end of file
+BaseModel = OAuthList
diff --git a/src/lib/browser_use/scanner.py b/src/lib/browser_use/scanner.py
index 04a9371..4e343e4 100644
--- a/src/lib/browser_use/scanner.py
+++ b/src/lib/browser_use/scanner.py
@@ -1,10 +1,21 @@
import asyncio
-import os
import csv
+import os
+
+from lib.browser_use.agents import (
+ extract_oauth_list,
+ get_retry_queue_status,
+ start_retry_queue_processor,
+ test_oauth_login,
+)
+from lib.utils import is_html_url, notify_backend, read_lines_between
+from lib.utils.progress import (
+ current_progress,
+ load_progress,
+ progress_file,
+ save_progress,
+)
-from lib.utils import notify_backend, read_lines_between, is_html_url
-from lib.browser_use.agents import extract_oauth_list, test_oauth_login, start_retry_queue_processor, get_retry_queue_status
-from lib.utils.progress import current_progress, load_progress, save_progress, progress_file
async def scan_one_url(url: str, skip_html_check: bool = False):
"""URL 스캔 통합 함수: OAuth 리스트 추출 → 개별 OAuth 로그인 시도"""
@@ -45,9 +56,7 @@ async def scan_one_url(url: str, skip_html_check: bool = False):
# 2단계: 각 OAuth 제공자별로 개별 로그인 시도
for i, oauth_entry in enumerate(oauth_entries):
- print(
- f"\n🔄 OAuth 로그인 테스트 {i+1}/{len(oauth_entries)}: {oauth_entry}"
- )
+ print(f"\n🔄 OAuth 로그인 테스트 {i+1}/{len(oauth_entries)}: {oauth_entry}")
# OAuth 간 대기 시간
if i > 0:
@@ -68,7 +77,7 @@ async def main_loop(
"""지정된 URL 목록에 대해 스캔을 실행하는 메인 루프"""
# 재시도 큐 처리기 시작
await start_retry_queue_processor()
-
+
target_list = read_lines_between(
filepath=filepath, start_line=start_line, end_line=end_line
)
@@ -82,11 +91,13 @@ async def main_loop(
prev_progress = load_progress()
if prev_progress and prev_progress.get("start_line") == start_line:
print("📋 이전 진행 상황을 발견했습니다:")
- print(f" - 이전 완료: {prev_progress['current_index']}/{prev_progress['total']}")
+ print(
+ f" - 이전 완료: {prev_progress['current_index']}/{prev_progress['total']}"
+ )
print(f" - 마지막 처리: {prev_progress.get('current_url', 'N/A')}")
resume = input("이어서 진행하시겠습니까? (y/n): ").lower().strip()
- if resume == 'y':
+ if resume == "y":
start_index = prev_progress.get("current_index", 0)
current_progress["current_index"] = start_index
# 전체 개수는 원래 목록 길이로 유지
@@ -98,9 +109,13 @@ async def main_loop(
# current_index는 전체 목록에서의 현재 위치를 나타냄
current_url_index = current_progress["current_index"]
current_progress["current_url"] = url
-
- print(f"\n🔄 Processing {current_url_index + 1}/{current_progress['total']}: {url}")
- print(f"📍 {os.path.basename(filepath)}의 {start_line + current_url_index}번째 줄")
+
+ print(
+ f"\n🔄 Processing {current_url_index + 1}/{current_progress['total']}: {url}"
+ )
+ print(
+ f"📍 {os.path.basename(filepath)}의 {start_line + current_url_index}번째 줄"
+ )
# 재시도 큐 상태 확인 및 출력
retry_status = await get_retry_queue_status()
@@ -116,7 +131,9 @@ async def main_loop(
# 스캔 완료 후 재시도 큐 상태 확인
retry_status_after = await get_retry_queue_status()
if retry_status_after["queue_length"] > 0:
- print(f"📊 스캔 완료 후 재시도 큐 상태: {retry_status_after['queue_length']}개 작업 대기 중")
+ print(
+ f"📊 스캔 완료 후 재시도 큐 상태: {retry_status_after['queue_length']}개 작업 대기 중"
+ )
# 다음 URL로 진행
current_progress["current_index"] = current_url_index + 1
@@ -128,8 +145,10 @@ async def main_loop(
retry_status = await get_retry_queue_status()
if retry_status["queue_length"] == 0:
break
- print(f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 남음. 30초 후 다시 확인...")
+ print(
+ f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 남음. 30초 후 다시 확인..."
+ )
await asyncio.sleep(30)
print(f"\n🎉 모든 스캔이 완료되었습니다! ({total_count}개 URL)")
- print("🎉 재시도 큐도 모두 처리되었습니다!")
\ No newline at end of file
+ print("🎉 재시도 큐도 모두 처리되었습니다!")
diff --git a/src/lib/browser_use/sensitive_data.py b/src/lib/browser_use/sensitive_data.py
index 7d4fbf4..41eb910 100644
--- a/src/lib/browser_use/sensitive_data.py
+++ b/src/lib/browser_use/sensitive_data.py
@@ -3,19 +3,20 @@
import json
import os
+
def GetSensitiveData():
"""
Reads sensitive data from a .sensitive.json file in the current directory.
-
+
Returns:
dict: A dictionary containing the sensitive data.
"""
- file_path = os.path.join(os.getcwd(), '.sensitive.json')
-
+ file_path = os.path.join(os.getcwd(), ".sensitive.json")
+
if not os.path.exists(file_path):
return None
-
- with open(file_path, 'r') as file:
+
+ with open(file_path, "r") as file:
sensitive_data = json.load(file)
-
- return sensitive_data
\ No newline at end of file
+
+ return sensitive_data
diff --git a/src/lib/llm/__init__.py b/src/lib/llm/__init__.py
index 9acc135..e6575ee 100644
--- a/src/lib/llm/__init__.py
+++ b/src/lib/llm/__init__.py
@@ -1,3 +1,2 @@
from lib.llm.create import *
-
-from lib.llm.prompt import *
\ No newline at end of file
+from lib.llm.prompt import *
diff --git a/src/lib/llm/create.py b/src/lib/llm/create.py
index 0e9b682..9588f6f 100644
--- a/src/lib/llm/create.py
+++ b/src/lib/llm/create.py
@@ -4,15 +4,16 @@ from dotenv import load_dotenv
# 환경 변수 로드 (GOOGLE_API_KEY 필요)
load_dotenv(override=True)
+
def CreateChatGoogle(model: str):
"""Browser Use용 Google 모델 생성"""
if model == "fallback":
print("⚠️ Fallback 모델을 사용합니다. Environment 변수를 확인하세요.")
print("⚠️ Model gemini-2.0-flash-lite를 사용합니다.")
model = "gemini-2.0-flash-lite"
-
+
return ChatGoogle(
model=model,
- temperature=0.0,
+ temperature=0.0
# Browser Use는 내부적으로 재시도 로직을 처리합니다
- )
\ No newline at end of file
+ )
diff --git a/src/lib/llm/prompt/__init__.py b/src/lib/llm/prompt/__init__.py
index 3d676a5..3ece5c2 100644
--- a/src/lib/llm/prompt/__init__.py
+++ b/src/lib/llm/prompt/__init__.py
@@ -1,6 +1,8 @@
-from typing import Union, Type
+from typing import Type, Union
+
from pydantic import BaseModel
+
def get_prompt(type: str) -> tuple[str, Type[BaseModel]] | str:
"""
Prompt를 반환합니다.
@@ -9,17 +11,36 @@ def get_prompt(type: str) -> tuple[str, Type[BaseModel]] | str:
:return: 해당하는 프롬프트 문자열 또는 (프롬프트, 모델) 튜플
"""
if type.lower() == "auth":
- from lib.llm.prompt.get_oauth import prompt, model
+ from lib.llm.prompt._get_oauth import model, prompt
+
+ return prompt, model
+
+ elif type.lower() in ["google", "google account"]:
+ from lib.llm.prompt.google import model, prompt
+
+ return prompt, model
+
+ elif type.lower() in ["microsoft", "microsoftonline"]:
+ from lib.llm.prompt.microsoft import model, prompt
+
+ return prompt, model
+
+ elif type.lower() in ["meta", "facebook"]:
+ from lib.llm.prompt.facebook import model, prompt
+
+ return prompt, model
+
+ elif type.lower() in ["apple"]:
+ from lib.llm.prompt.apple import model, prompt
+
+ return prompt, model
+
+ elif type.lower() in ["github"]:
+ from lib.llm.prompt.github import model, prompt
+
return prompt, model
-
- # elif type.lower() in ["google", "google account"]:
- # from lib.llm.prompt.google import prompt, model
- # return prompt, model
-
- # elif type.lower() in ["microsoft", "microsoftonline"]:
- # from lib.llm.prompt.microsoft import prompt, model
- # return prompt, model
else:
- from lib.llm.prompt.fallback import model, prompt
+ from lib.llm.prompt._fallback import model, prompt
+
return prompt, model
diff --git a/src/lib/llm/prompt/_fallback/__init__.py b/src/lib/llm/prompt/_fallback/__init__.py
new file mode 100644
index 0000000..298d321
--- /dev/null
+++ b/src/lib/llm/prompt/_fallback/__init__.py
@@ -0,0 +1,2 @@
+from lib.llm.prompt._fallback.model import model
+from lib.llm.prompt._fallback.prompt import prompt
diff --git a/src/lib/llm/prompt/_fallback/model.py b/src/lib/llm/prompt/_fallback/model.py
new file mode 100644
index 0000000..7a3541e
--- /dev/null
+++ b/src/lib/llm/prompt/_fallback/model.py
@@ -0,0 +1,9 @@
+from pydantic import BaseModel
+
+
+class model(BaseModel):
+ msg: str | None = None
+ status: str | None = (
+ None # "success", "mfa_required", "blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ )
+ final_url: str | None = None
diff --git a/src/lib/llm/prompt/fallback/prompt.py b/src/lib/llm/prompt/_fallback/prompt.py
similarity index 100%
rename from src/lib/llm/prompt/fallback/prompt.py
rename to src/lib/llm/prompt/_fallback/prompt.py
diff --git a/src/lib/llm/prompt/_get_oauth/__init__.py b/src/lib/llm/prompt/_get_oauth/__init__.py
new file mode 100644
index 0000000..ac7a283
--- /dev/null
+++ b/src/lib/llm/prompt/_get_oauth/__init__.py
@@ -0,0 +1,2 @@
+from lib.llm.prompt._get_oauth.model import model
+from lib.llm.prompt._get_oauth.prompt import prompt
diff --git a/src/lib/llm/prompt/get_oauth/model.py b/src/lib/llm/prompt/_get_oauth/model.py
similarity index 99%
rename from src/lib/llm/prompt/get_oauth/model.py
rename to src/lib/llm/prompt/_get_oauth/model.py
index b8980d3..51c822c 100644
--- a/src/lib/llm/prompt/get_oauth/model.py
+++ b/src/lib/llm/prompt/_get_oauth/model.py
@@ -1,5 +1,6 @@
from pydantic import BaseModel
+
class model(BaseModel):
msg: str | None = None
url: str | None = None
diff --git a/src/lib/llm/prompt/get_oauth/prompt.py b/src/lib/llm/prompt/_get_oauth/prompt.py
similarity index 100%
rename from src/lib/llm/prompt/get_oauth/prompt.py
rename to src/lib/llm/prompt/_get_oauth/prompt.py
diff --git a/src/lib/llm/prompt/apple/__init__.py b/src/lib/llm/prompt/apple/__init__.py
index ccd832f..972bc5a 100644
--- a/src/lib/llm/prompt/apple/__init__.py
+++ b/src/lib/llm/prompt/apple/__init__.py
@@ -1,2 +1,2 @@
-from lib.llm.prompt.apple.prompt import prompt
from lib.llm.prompt.apple.model import model
+from lib.llm.prompt.apple.prompt import prompt
diff --git a/src/lib/llm/prompt/apple/model.py b/src/lib/llm/prompt/apple/model.py
index 773f3a7..087dc34 100644
--- a/src/lib/llm/prompt/apple/model.py
+++ b/src/lib/llm/prompt/apple/model.py
@@ -1,6 +1,9 @@
from pydantic import BaseModel
+
class model(BaseModel):
msg: str | None = None
- status: str | None = None # "success", "mfa_required", "apple_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ status: str | None = (
+ None # "success", "mfa_required", "apple_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ )
final_url: str | None = None
diff --git a/src/lib/llm/prompt/apple/prompt.py b/src/lib/llm/prompt/apple/prompt.py
index efd2546..8ba8333 100644
--- a/src/lib/llm/prompt/apple/prompt.py
+++ b/src/lib/llm/prompt/apple/prompt.py
@@ -56,4 +56,4 @@ Return the result in the following format only:
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
-"""
\ No newline at end of file
+"""
diff --git a/src/lib/llm/prompt/facebook/__init__.py b/src/lib/llm/prompt/facebook/__init__.py
index 7a7672b..c609cfc 100644
--- a/src/lib/llm/prompt/facebook/__init__.py
+++ b/src/lib/llm/prompt/facebook/__init__.py
@@ -1,2 +1,2 @@
+from lib.llm.prompt.facebook.model import model
from lib.llm.prompt.facebook.prompt import prompt
-from lib.llm.prompt.facebook.model import model
\ No newline at end of file
diff --git a/src/lib/llm/prompt/facebook/model.py b/src/lib/llm/prompt/facebook/model.py
index ef01814..850c683 100644
--- a/src/lib/llm/prompt/facebook/model.py
+++ b/src/lib/llm/prompt/facebook/model.py
@@ -1,6 +1,9 @@
from pydantic import BaseModel
+
class model(BaseModel):
msg: str | None = None
- status: str | None = None # "success", "mfa_required", "facebook_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
- final_url: str | None = None
\ No newline at end of file
+ status: str | None = (
+ None # "success", "mfa_required", "facebook_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ )
+ final_url: str | None = None
diff --git a/src/lib/llm/prompt/facebook/prompt.py b/src/lib/llm/prompt/facebook/prompt.py
index 518dd4f..b084e84 100644
--- a/src/lib/llm/prompt/facebook/prompt.py
+++ b/src/lib/llm/prompt/facebook/prompt.py
@@ -1,4 +1,5 @@
import os
+
# Extended planner prompt
prompt = f"""
You are a web automation agent.
@@ -47,4 +48,4 @@ Return the result in the following format only:
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
-"""
\ No newline at end of file
+"""
diff --git a/src/lib/llm/prompt/fallback/__init__.py b/src/lib/llm/prompt/fallback/__init__.py
deleted file mode 100644
index c9a486e..0000000
--- a/src/lib/llm/prompt/fallback/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from lib.llm.prompt.fallback.prompt import prompt
-from lib.llm.prompt.fallback.model import model
diff --git a/src/lib/llm/prompt/fallback/model.py b/src/lib/llm/prompt/fallback/model.py
deleted file mode 100644
index 832409c..0000000
--- a/src/lib/llm/prompt/fallback/model.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from pydantic import BaseModel
-
-class model(BaseModel):
- msg: str | None = None
- status: str | None = None # "success", "mfa_required", "blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
- final_url: str | None = None
diff --git a/src/lib/llm/prompt/get_oauth/__init__.py b/src/lib/llm/prompt/get_oauth/__init__.py
deleted file mode 100644
index 1759f89..0000000
--- a/src/lib/llm/prompt/get_oauth/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from lib.llm.prompt.get_oauth.prompt import prompt
-from lib.llm.prompt.get_oauth.model import model
diff --git a/src/lib/llm/prompt/github/__init__.py b/src/lib/llm/prompt/github/__init__.py
index b6faf4a..eb94965 100644
--- a/src/lib/llm/prompt/github/__init__.py
+++ b/src/lib/llm/prompt/github/__init__.py
@@ -1,2 +1,2 @@
+from lib.llm.prompt.github.model import model
from lib.llm.prompt.github.prompt import prompt
-from lib.llm.prompt.github.model import model
\ No newline at end of file
diff --git a/src/lib/llm/prompt/github/model.py b/src/lib/llm/prompt/github/model.py
index de7ecc5..62cdf22 100644
--- a/src/lib/llm/prompt/github/model.py
+++ b/src/lib/llm/prompt/github/model.py
@@ -1,6 +1,9 @@
from pydantic import BaseModel
+
class model(BaseModel):
msg: str | None = None
- status: str | None = None # "success", "mfa_required", "github_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ status: str | None = (
+ None # "success", "mfa_required", "github_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ )
final_url: str | None = None
diff --git a/src/lib/llm/prompt/github/prompt.py b/src/lib/llm/prompt/github/prompt.py
index 2ab247d..a4e22d8 100644
--- a/src/lib/llm/prompt/github/prompt.py
+++ b/src/lib/llm/prompt/github/prompt.py
@@ -67,4 +67,4 @@ Return the result in the following format only:
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
-"""
\ No newline at end of file
+"""
diff --git a/src/lib/llm/prompt/google/__init__.py b/src/lib/llm/prompt/google/__init__.py
index 86311ab..0b8309a 100644
--- a/src/lib/llm/prompt/google/__init__.py
+++ b/src/lib/llm/prompt/google/__init__.py
@@ -1,2 +1,2 @@
-from lib.llm.prompt.google.prompt import prompt
from lib.llm.prompt.google.model import model
+from lib.llm.prompt.google.prompt import prompt
diff --git a/src/lib/llm/prompt/google/model.py b/src/lib/llm/prompt/google/model.py
index d1322ba..24cd5c4 100644
--- a/src/lib/llm/prompt/google/model.py
+++ b/src/lib/llm/prompt/google/model.py
@@ -1,6 +1,9 @@
from pydantic import BaseModel
+
class model(BaseModel):
msg: str | None = None
- status: str | None = None # "success", "mfa_required", "google_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ status: str | None = (
+ None # "success", "mfa_required", "google_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ )
final_url: str | None = None
diff --git a/src/lib/llm/prompt/google/prompt.py b/src/lib/llm/prompt/google/prompt.py
index d1dcd24..fbbf879 100644
--- a/src/lib/llm/prompt/google/prompt.py
+++ b/src/lib/llm/prompt/google/prompt.py
@@ -55,4 +55,4 @@ Return the result in the following format only:
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
-"""
\ No newline at end of file
+"""
diff --git a/src/lib/llm/prompt/microsoft/__init__.py b/src/lib/llm/prompt/microsoft/__init__.py
index dc3dca2..570cc5c 100644
--- a/src/lib/llm/prompt/microsoft/__init__.py
+++ b/src/lib/llm/prompt/microsoft/__init__.py
@@ -1,2 +1,2 @@
-from lib.llm.prompt.microsoft.prompt import prompt
from lib.llm.prompt.microsoft.model import model
+from lib.llm.prompt.microsoft.prompt import prompt
diff --git a/src/lib/llm/prompt/microsoft/model.py b/src/lib/llm/prompt/microsoft/model.py
index 65cee9a..431b08e 100644
--- a/src/lib/llm/prompt/microsoft/model.py
+++ b/src/lib/llm/prompt/microsoft/model.py
@@ -1,6 +1,9 @@
from pydantic import BaseModel
+
class model(BaseModel):
msg: str | None = None
- status: str | None = None # "success", "mfa_required", "microsoft_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ status: str | None = (
+ None # "success", "mfa_required", "microsoft_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
+ )
final_url: str | None = None
diff --git a/src/lib/llm/prompt/microsoft/prompt.py b/src/lib/llm/prompt/microsoft/prompt.py
index e093b63..a9d15a0 100644
--- a/src/lib/llm/prompt/microsoft/prompt.py
+++ b/src/lib/llm/prompt/microsoft/prompt.py
@@ -54,4 +54,4 @@ Microsoft 로그인에 사용할 자격 증명:
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
-"""
\ No newline at end of file
+"""
diff --git a/src/lib/utils/__init__.py b/src/lib/utils/__init__.py
index 4a068ce..b440797 100644
--- a/src/lib/utils/__init__.py
+++ b/src/lib/utils/__init__.py
@@ -1,7 +1,7 @@
# export from show_info
from lib.utils.agent_info import *
-from lib.utils.data import *
from lib.utils.config import *
+from lib.utils.data import *
from lib.utils.parsing.is_html import *
from lib.utils.parsing.read_txt import *
diff --git a/src/lib/utils/agent_info.py b/src/lib/utils/agent_info.py
index ea56116..b8c9248 100644
--- a/src/lib/utils/agent_info.py
+++ b/src/lib/utils/agent_info.py
@@ -1,13 +1,17 @@
+import os
+
+from dotenv import load_dotenv
+
from lib.utils.config import (
BACKEND_URL,
GOOGLE_API_KEY,
GOOGLE_MODEL,
GOOGLE_PLANNER_MODEL,
)
-import os
-from dotenv import load_dotenv
+
load_dotenv(override=True)
+
def show_info():
print("🔧 환경 설정:")
print(browser_use_version())
@@ -40,7 +44,10 @@ def browser_use_version():
def env_cheker():
if GOOGLE_API_KEY is None:
raise ValueError("GOOGLE_API_KEY 환경변수가 설정되지 않았습니다.")
- if GOOGLE_PLANNER_MODEL != None and (not os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LOGIN") or not os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LIST")):
+ if GOOGLE_PLANNER_MODEL != None and (
+ not os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LOGIN")
+ or not os.getenv("ENABLE_PLANNER_MODEL_OAUTH_LIST")
+ ):
print(
"⚠️ GOOGLE_PLANNER_MODEL이 설정되어 있지만, ENABLE_PLANNER_MODEL_OAUTH_LOGIN 또는 ENABLE_PLANNER_MODEL_OAUTH_LIST가 활성화되지 않았습니다."
)
@@ -50,9 +57,8 @@ def env_cheker():
print(
"‼️ 하지만 현재 Planner 모델을 사용하는 것이 권장되지 않습니다. 이 기능은 오작동을 일으킬 수 있습니다."
)
- print(
- "⚠️ 이 경고는 1초동안 정지합니다."
- )
+ print("⚠️ 이 경고는 1초동안 정지합니다.")
# 이 경고는 1초동안 sleep
import time
+
time.sleep(1)
diff --git a/src/lib/utils/config.py b/src/lib/utils/config.py
index 9066ad6..c4dde2a 100644
--- a/src/lib/utils/config.py
+++ b/src/lib/utils/config.py
@@ -1,8 +1,11 @@
import os
+
from dotenv import load_dotenv
+
load_dotenv(verbose=True, override=True)
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:11081")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.5-flash")
-GOOGLE_PLANNER_MODEL = os.getenv("GOOGLE_PLANNER_MODEL")
\ No newline at end of file
+GOOGLE_PLANNER_MODEL = os.getenv("GOOGLE_PLANNER_MODEL")
+USER_DATA_DIR = os.getenv("USER_DATA_DIR", "./data/user_data")
\ No newline at end of file
diff --git a/src/lib/utils/data/backend_client.py b/src/lib/utils/data/backend_client.py
index 68f497e..4548541 100644
--- a/src/lib/utils/data/backend_client.py
+++ b/src/lib/utils/data/backend_client.py
@@ -2,6 +2,7 @@ import requests
from lib.utils.config import BACKEND_URL
+
def notify_backend(target_url):
# Backend에 스캔 시작을 알림
try:
diff --git a/src/lib/utils/data/logger.py b/src/lib/utils/data/logger.py
index da61d65..5711db6 100644
--- a/src/lib/utils/data/logger.py
+++ b/src/lib/utils/data/logger.py
@@ -1,9 +1,10 @@
-from pathlib import Path
from datetime import datetime
+from pathlib import Path
# 미리 정해진 파일 경로
FILE_PATH = Path("data/log.txt")
+
def logger(msg: str) -> None:
try:
"""
@@ -13,7 +14,7 @@ def logger(msg: str) -> None:
"""
# 상위 디렉터리 생성 (이미 있으면 무시)
FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
-
+
# 현재 시각 구해서 포맷팅
now = datetime.now()
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
@@ -26,4 +27,4 @@ def logger(msg: str) -> None:
with FILE_PATH.open(mode="a", encoding="utf-8") as f:
f.write(line)
except:
- print(msg)
\ No newline at end of file
+ print(msg)
diff --git a/src/lib/utils/parsing/is_html.py b/src/lib/utils/parsing/is_html.py
index 495af22..08b72ae 100644
--- a/src/lib/utils/parsing/is_html.py
+++ b/src/lib/utils/parsing/is_html.py
@@ -1,32 +1,34 @@
import requests
+
def is_html_url(url: str, timeout: float = 10.0) -> bool:
"""
주어진 URL에 HEAD 요청을 보내고, 응답 헤더의 Content-Type이 HTML인지 확인합니다.
- url: 검사할 URL 문자열
- timeout: 요청 타임아웃(초 단위)
-
+
반환값:
- Content-Type이 'text/html' 로 시작하면 True, 그렇지 않으면 False
"""
-
+
try:
with requests.get(url, timeout=timeout, stream=True) as response:
# 응답 코드가 200번대가 아니면 False로 간주
if not response.ok:
return False
- content_type = response.headers.get('Content-Type', '')
+ content_type = response.headers.get("Content-Type", "")
# Content-Type에 'text/html'이 포함되어 있으면 HTML로 간주
- return content_type.lower().startswith('text/html')
+ return content_type.lower().startswith("text/html")
except requests.RequestException:
return False
-if __name__ == '__main__':
+
+if __name__ == "__main__":
test_urls = [
- 'https://www.example.com',
- 'https://api.github.com', # JSON API라서 HTML이 아닐 확률이 높음
- 'https://raw.githubusercontent.com' # 텍스트 파일 등 다양한 타입
+ "https://www.example.com",
+ "https://api.github.com", # JSON API라서 HTML이 아닐 확률이 높음
+ "https://raw.githubusercontent.com", # 텍스트 파일 등 다양한 타입
]
for url in test_urls:
diff --git a/src/lib/utils/parsing/read_txt.py b/src/lib/utils/parsing/read_txt.py
index 9cb1aa0..f3b356d 100644
--- a/src/lib/utils/parsing/read_txt.py
+++ b/src/lib/utils/parsing/read_txt.py
@@ -1,6 +1,6 @@
def read_lines_between(filepath: str, start_line: int, end_line: int) -> list[str]:
"""
- 파일에서 start_line번 째 줄부터 end_line번 째 줄까지 읽어와
+ 파일에서 start_line번 째 줄부터 end_line번 째 줄까지 읽어와
각 줄을 요소로 갖는 리스트를 반환하는 함수.
Parameters:
@@ -15,15 +15,17 @@ def read_lines_between(filepath: str, start_line: int, end_line: int) -> list[st
Returns:
-------
list[str]
- 각 줄을 문자열로 저장한 리스트.
+ 각 줄을 문자열로 저장한 리스트.
파일에 해당 범위의 줄이 없으면 가능한 만큼만 반환.
"""
if start_line < 1 or end_line < start_line:
- raise ValueError("start_line은 1 이상이어야 하며, end_line은 start_line 이상이어야 합니다.")
+ raise ValueError(
+ "start_line은 1 이상이어야 하며, end_line은 start_line 이상이어야 합니다."
+ )
selected_lines: list[str] = []
- with open(filepath, 'r', encoding='utf-8') as f:
+ with open(filepath, "r", encoding="utf-8") as f:
for idx, line in enumerate(f, start=1):
if idx < start_line:
# 아직 읽기 시작 전
@@ -32,5 +34,5 @@ def read_lines_between(filepath: str, start_line: int, end_line: int) -> list[st
# 읽을 범위를 벗어났으므로 중단
break
# 줄 끝의 개행 문자를 제거하고 리스트에 추가
- selected_lines.append(line.rstrip('\n'))
+ selected_lines.append(line.rstrip("\n"))
return selected_lines
diff --git a/src/lib/utils/progress.py b/src/lib/utils/progress.py
index 1803ad4..157106d 100644
--- a/src/lib/utils/progress.py
+++ b/src/lib/utils/progress.py
@@ -7,12 +7,14 @@ from pathlib import Path
current_progress = {"current_index": 0, "total": 0, "current_url": "", "start_line": 0}
progress_file = Path("data/scan_progress.json")
+
def save_progress():
"""현재 진행 상황을 파일에 저장"""
progress_file.parent.mkdir(parents=True, exist_ok=True)
with open(progress_file, "w", encoding="utf-8") as f:
json.dump(current_progress, f, ensure_ascii=False, indent=2)
+
def load_progress():
"""이전 진행 상황을 파일에서 불러오기"""
if os.path.exists(progress_file):
@@ -23,6 +25,7 @@ def load_progress():
return None
return None
+
def signal_handler(signum, frame):
"""Ctrl+C 시그널 핸들러"""
print("\n" + "=" * 60)
@@ -34,7 +37,7 @@ def signal_handler(signum, frame):
print(
f" - domains.txt의 {current_progress['start_line'] + current_progress['current_index']}번째 줄"
)
- if current_progress['total'] > 0:
+ if current_progress["total"] > 0:
print(
f" - 진행률: {current_progress['current_index']}/{current_progress['total']} ({current_progress['current_index']/current_progress['total']*100:.1f}%)"
)
@@ -43,6 +46,7 @@ def signal_handler(signum, frame):
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
exit(0)
+
def setup_signal_handler():
"""시그널 핸들러 등록"""
- signal.signal(signal.SIGINT, signal_handler)
\ No newline at end of file
+ signal.signal(signal.SIGINT, signal_handler)
diff --git a/src/main.py b/src/main.py
index c692f28..a42f201 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,32 +1,35 @@
-import asyncio
import argparse
+import asyncio
import os
+import sys
+
from dotenv import load_dotenv
-from lib.utils import env_cheker
from lib.browser_use.scanner import main_loop
-from lib.utils.progress import setup_signal_handler, progress_file
-
-# .env 파일 로드
-load_dotenv(verbose=True, override=True)
-
-# 환경 변수 체크
-env_cheker()
-
-# Laminar 초기화 (선택적)
-if os.getenv("LMNR_PROJECT_API_KEY"):
- try:
- from lmnr import Laminar
- Laminar.initialize(project_api_key=os.getenv("LMNR_PROJECT_API_KEY"))
- except ImportError:
- print("⚠️ Laminar 라이브러리가 설치되지 않았습니다. 관련 기능이 비활성화됩니다.")
+from lib.utils import env_cheker
+from lib.utils.progress import progress_file, setup_signal_handler
-def main():
- """애플리케이션 메인 진입점"""
- # 시그널 핸들러 설정
- setup_signal_handler()
+def setup_environment():
+ """환경 변수 로드 및 관련 라이브러리를 초기화합니다."""
+ # .env 파일 로드
+ load_dotenv(verbose=True, override=True)
+ # 환경 변수 체크
+ env_cheker()
+
+ # Laminar 초기화 (선택적)
+ if os.getenv("LMNR_PROJECT_API_KEY"):
+ try:
+ from lmnr import Laminar
+
+ Laminar.initialize(project_api_key=os.getenv("LMNR_PROJECT_API_KEY"))
+ except ImportError:
+ print("⚠️ Laminar 라이브러리가 설치되지 않았습니다. 관련 기능이 비활성화됩니다.")
+
+
+def parse_arguments():
+ """커맨드 라인 인자를 파싱합니다."""
parser = argparse.ArgumentParser(
prog="domain_scanner",
description="도메인 목록 파일에서 지정한 줄 범위를 읽어 SSO 스캔을 수행합니다.",
@@ -48,11 +51,18 @@ def main():
parser.add_argument(
"-skh",
"--skip-html-check",
- action='store_true', # 플래그 형식으로 변경
+ action="store_true",
help="HTML 페이지 체크를 건너뛰고 모든 URL을 스캔합니다.",
)
- args = parser.parse_args()
+ return parser.parse_args()
+
+
+def main():
+ """애플리케이션 메인 진입점"""
+ setup_environment()
+ setup_signal_handler()
+ args = parse_arguments()
try:
asyncio.run(
@@ -64,16 +74,17 @@ def main():
)
)
except KeyboardInterrupt:
- # signal_handler가 처리하므로 여기서는 별도 처리 불필요
- pass
+ print("\n프로그램이 사용자에 의해 중단되었습니다.")
+ sys.exit(1)
finally:
# 정상 종료 시 진행 상황 파일 삭제
if os.path.exists(progress_file):
try:
os.remove(progress_file)
+ print("진행 상황 파일이 삭제되었습니다.")
except OSError as e:
- print(f"오류: 진행 상황 파일을 삭제하지 못했습니다. {e}")
+ print(f"오류: 진행 상황 파일을 삭제하지 못했습니다. {e}", file=sys.stderr)
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/uv.lock b/uv.lock
index 9589154..6248d6c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -94,6 +94,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
]
+[[package]]
+name = "black"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" },
+ { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" },
+ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" },
+]
+
[[package]]
name = "browser-use"
version = "0.3.3"
@@ -140,16 +160,20 @@ name = "browser-use-test"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "black" },
{ name = "browser-use", extra = ["memory"] },
{ name = "chardet" },
+ { name = "isort" },
{ name = "lmnr", extra = ["all"] },
{ name = "patchright" },
]
[package.metadata]
requires-dist = [
+ { name = "black", specifier = ">=25.1.0" },
{ name = "browser-use", extras = ["memory"], specifier = "==0.3.3" },
{ name = "chardet", specifier = ">=5.2.0" },
+ { name = "isort", specifier = ">=6.0.1" },
{ name = "lmnr", extras = ["all"], specifier = ">=0.6.10" },
{ name = "patchright", specifier = ">=1.52.5" },
]
@@ -241,6 +265,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -582,6 +618,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
]
+[[package]]
+name = "isort"
+version = "6.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" },
+]
+
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -619,11 +664,11 @@ wheels = [
[[package]]
name = "joblib"
-version = "1.5.0"
+version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/08/8bd4a0250247861420a040b33ccf42f43c426ac91d99405374ef117e5872/joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5", size = 330234, upload-time = "2025-05-03T21:09:39.553Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/da/d3/13ee227a148af1c693654932b8b0b02ed64af5e1f7406d56b088b57574cd/joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491", size = 307682, upload-time = "2025-05-03T21:09:37.892Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" },
]
[[package]]
@@ -759,12 +804,21 @@ wheels = [
]
[[package]]
-name = "networkx"
-version = "3.4.2"
+name = "mypy-extensions"
+version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" },
]
[[package]]
@@ -1523,33 +1577,68 @@ wheels = [
]
[[package]]
-name = "pillow"
-version = "11.2.1"
+name = "pathspec"
+version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" },
- { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" },
- { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" },
- { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" },
- { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" },
- { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" },
- { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" },
- { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" },
- { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" },
- { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" },
- { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" },
- { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" },
- { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" },
- { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" },
- { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" },
- { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" },
- { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" },
- { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" },
- { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" },
- { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" },
- { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" },
- { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
+ { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
+ { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
+ { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
+ { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
+ { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
+ { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
+ { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
+ { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
+ { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
+ { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
[[package]]
@@ -4277,7 +4366,7 @@ wheels = [
[[package]]
name = "scikit-learn"
-version = "1.6.1"
+version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "joblib" },
@@ -4285,46 +4374,46 @@ dependencies = [
{ name = "scipy" },
{ name = "threadpoolctl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/3b/29fa87e76b1d7b3b77cc1fcbe82e6e6b8cd704410705b008822de530277c/scikit_learn-1.7.0.tar.gz", hash = "sha256:c01e869b15aec88e2cdb73d27f15bdbe03bce8e2fb43afbe77c45d399e73a5a3", size = 7178217, upload-time = "2025-06-05T22:02:46.703Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001, upload-time = "2025-01-10T08:06:58.613Z" },
- { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360, upload-time = "2025-01-10T08:07:01.556Z" },
- { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004, upload-time = "2025-01-10T08:07:06.931Z" },
- { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776, upload-time = "2025-01-10T08:07:11.715Z" },
- { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865, upload-time = "2025-01-10T08:07:16.088Z" },
- { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804, upload-time = "2025-01-10T08:07:20.385Z" },
- { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530, upload-time = "2025-01-10T08:07:23.675Z" },
- { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852, upload-time = "2025-01-10T08:07:26.817Z" },
- { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256, upload-time = "2025-01-10T08:07:31.084Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/c3/a85dcccdaf1e807e6f067fa95788a6485b0491d9ea44fd4c812050d04f45/scikit_learn-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b7974f1f32bc586c90145df51130e02267e4b7e77cab76165c76cf43faca0d9", size = 11559841, upload-time = "2025-06-05T22:02:23.308Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/57/eea0de1562cc52d3196eae51a68c5736a31949a465f0b6bb3579b2d80282/scikit_learn-1.7.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:014e07a23fe02e65f9392898143c542a50b6001dbe89cb867e19688e468d049b", size = 10616463, upload-time = "2025-06-05T22:02:26.068Z" },
+ { url = "https://files.pythonhosted.org/packages/10/a4/39717ca669296dfc3a62928393168da88ac9d8cbec88b6321ffa62c6776f/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e7ced20582d3a5516fb6f405fd1d254e1f5ce712bfef2589f51326af6346e8", size = 11766512, upload-time = "2025-06-05T22:02:28.689Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/cd/a19722241d5f7b51e08351e1e82453e0057aeb7621b17805f31fcb57bb6c/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1babf2511e6ffd695da7a983b4e4d6de45dce39577b26b721610711081850906", size = 12461075, upload-time = "2025-06-05T22:02:31.233Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/bc/282514272815c827a9acacbe5b99f4f1a4bc5961053719d319480aee0812/scikit_learn-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:5abd2acff939d5bd4701283f009b01496832d50ddafa83c90125a4e41c33e314", size = 10652517, upload-time = "2025-06-05T22:02:34.139Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/78/7357d12b2e4c6674175f9a09a3ba10498cde8340e622715bcc71e532981d/scikit_learn-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e39d95a929b112047c25b775035c8c234c5ca67e681ce60d12413afb501129f7", size = 12111822, upload-time = "2025-06-05T22:02:36.904Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/0c/9c3715393343f04232f9d81fe540eb3831d0b4ec351135a145855295110f/scikit_learn-1.7.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:0521cb460426c56fee7e07f9365b0f45ec8ca7b2d696534ac98bfb85e7ae4775", size = 11325286, upload-time = "2025-06-05T22:02:39.739Z" },
+ { url = "https://files.pythonhosted.org/packages/64/e0/42282ad3dd70b7c1a5f65c412ac3841f6543502a8d6263cae7b466612dc9/scikit_learn-1.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317ca9f83acbde2883bd6bb27116a741bfcb371369706b4f9973cf30e9a03b0d", size = 12380865, upload-time = "2025-06-05T22:02:42.137Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d0/3ef4ab2c6be4aa910445cd09c5ef0b44512e3de2cfb2112a88bb647d2cf7/scikit_learn-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:126c09740a6f016e815ab985b21e3a0656835414521c81fc1a8da78b679bdb75", size = 11549609, upload-time = "2025-06-05T22:02:44.483Z" },
]
[[package]]
name = "scipy"
-version = "1.15.3"
+version = "1.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/18/b06a83f0c5ee8cddbde5e3f3d0bb9b702abfa5136ef6d4620ff67df7eee5/scipy-1.16.0.tar.gz", hash = "sha256:b5ef54021e832869c8cfb03bc3bf20366cbcd426e02a58e8a58d7584dfbb8f62", size = 30581216, upload-time = "2025-06-22T16:27:55.782Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" },
- { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" },
- { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" },
- { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" },
- { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" },
- { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" },
- { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" },
- { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" },
- { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" },
- { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" },
- { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" },
- { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" },
- { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" },
- { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" },
- { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" },
- { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" },
- { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" },
+ { url = "https://files.pythonhosted.org/packages/46/95/0746417bc24be0c2a7b7563946d61f670a3b491b76adede420e9d173841f/scipy-1.16.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:e9f414cbe9ca289a73e0cc92e33a6a791469b6619c240aa32ee18abdce8ab451", size = 36418162, upload-time = "2025-06-22T16:19:56.3Z" },
+ { url = "https://files.pythonhosted.org/packages/19/5a/914355a74481b8e4bbccf67259bbde171348a3f160b67b4945fbc5f5c1e5/scipy-1.16.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:bbba55fb97ba3cdef9b1ee973f06b09d518c0c7c66a009c729c7d1592be1935e", size = 28465985, upload-time = "2025-06-22T16:20:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/46/63477fc1246063855969cbefdcee8c648ba4b17f67370bd542ba56368d0b/scipy-1.16.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:58e0d4354eacb6004e7aa1cd350e5514bd0270acaa8d5b36c0627bb3bb486974", size = 20737961, upload-time = "2025-06-22T16:20:05.913Z" },
+ { url = "https://files.pythonhosted.org/packages/93/86/0fbb5588b73555e40f9d3d6dde24ee6fac7d8e301a27f6f0cab9d8f66ff2/scipy-1.16.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b2094ec975c80efc273567436e16bb794660509c12c6a31eb5c195cbf4b6dc", size = 23377941, upload-time = "2025-06-22T16:20:10.668Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/80/a561f2bf4c2da89fa631b3cbf31d120e21ea95db71fd9ec00cb0247c7a93/scipy-1.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b65d232157a380fdd11a560e7e21cde34fdb69d65c09cb87f6cc024ee376351", size = 33196703, upload-time = "2025-06-22T16:20:16.097Z" },
+ { url = "https://files.pythonhosted.org/packages/11/6b/3443abcd0707d52e48eb315e33cc669a95e29fc102229919646f5a501171/scipy-1.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d8747f7736accd39289943f7fe53a8333be7f15a82eea08e4afe47d79568c32", size = 35083410, upload-time = "2025-06-22T16:20:21.734Z" },
+ { url = "https://files.pythonhosted.org/packages/20/ab/eb0fc00e1e48961f1bd69b7ad7e7266896fe5bad4ead91b5fc6b3561bba4/scipy-1.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb9f147a1b8529bb7fec2a85cf4cf42bdfadf9e83535c309a11fdae598c88e8b", size = 35387829, upload-time = "2025-06-22T16:20:27.548Z" },
+ { url = "https://files.pythonhosted.org/packages/57/9e/d6fc64e41fad5d481c029ee5a49eefc17f0b8071d636a02ceee44d4a0de2/scipy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d2b83c37edbfa837a8923d19c749c1935ad3d41cf196006a24ed44dba2ec4358", size = 37841356, upload-time = "2025-06-22T16:20:35.112Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a7/4c94bbe91f12126b8bf6709b2471900577b7373a4fd1f431f28ba6f81115/scipy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:79a3c13d43c95aa80b87328a46031cf52508cf5f4df2767602c984ed1d3c6bbe", size = 38403710, upload-time = "2025-06-22T16:21:54.473Z" },
+ { url = "https://files.pythonhosted.org/packages/47/20/965da8497f6226e8fa90ad3447b82ed0e28d942532e92dd8b91b43f100d4/scipy-1.16.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:f91b87e1689f0370690e8470916fe1b2308e5b2061317ff76977c8f836452a47", size = 36813833, upload-time = "2025-06-22T16:20:43.925Z" },
+ { url = "https://files.pythonhosted.org/packages/28/f4/197580c3dac2d234e948806e164601c2df6f0078ed9f5ad4a62685b7c331/scipy-1.16.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:88a6ca658fb94640079e7a50b2ad3b67e33ef0f40e70bdb7dc22017dae73ac08", size = 28974431, upload-time = "2025-06-22T16:20:51.302Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/fc/e18b8550048d9224426e76906694c60028dbdb65d28b1372b5503914b89d/scipy-1.16.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ae902626972f1bd7e4e86f58fd72322d7f4ec7b0cfc17b15d4b7006efc385176", size = 21246454, upload-time = "2025-06-22T16:20:57.276Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/48/07b97d167e0d6a324bfd7484cd0c209cc27338b67e5deadae578cf48e809/scipy-1.16.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:8cb824c1fc75ef29893bc32b3ddd7b11cf9ab13c1127fe26413a05953b8c32ed", size = 23772979, upload-time = "2025-06-22T16:21:03.363Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/4f/9efbd3f70baf9582edf271db3002b7882c875ddd37dc97f0f675ad68679f/scipy-1.16.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:de2db7250ff6514366a9709c2cba35cb6d08498e961cba20d7cff98a7ee88938", size = 33341972, upload-time = "2025-06-22T16:21:11.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/dc/9e496a3c5dbe24e76ee24525155ab7f659c20180bab058ef2c5fa7d9119c/scipy-1.16.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e85800274edf4db8dd2e4e93034f92d1b05c9421220e7ded9988b16976f849c1", size = 35185476, upload-time = "2025-06-22T16:21:19.156Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b3/21001cff985a122ba434c33f2c9d7d1dc3b669827e94f4fc4e1fe8b9dfd8/scipy-1.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4f720300a3024c237ace1cb11f9a84c38beb19616ba7c4cdcd771047a10a1706", size = 35570990, upload-time = "2025-06-22T16:21:27.797Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d3/7ba42647d6709251cdf97043d0c107e0317e152fa2f76873b656b509ff55/scipy-1.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aad603e9339ddb676409b104c48a027e9916ce0d2838830691f39552b38a352e", size = 37950262, upload-time = "2025-06-22T16:21:36.976Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c4/231cac7a8385394ebbbb4f1ca662203e9d8c332825ab4f36ffc3ead09a42/scipy-1.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f56296fefca67ba605fd74d12f7bd23636267731a72cb3947963e76b8c0a25db", size = 38515076, upload-time = "2025-06-22T16:21:45.694Z" },
]
[[package]]
@@ -4342,7 +4431,7 @@ wheels = [
[[package]]
name = "sentence-transformers"
-version = "4.1.0"
+version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
@@ -4354,18 +4443,18 @@ dependencies = [
{ name = "transformers" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/73/84/b30d1b29ff58cfdff423e36a50efd622c8e31d7039b1a0d5e72066620da1/sentence_transformers-4.1.0.tar.gz", hash = "sha256:f125ffd1c727533e0eca5d4567de72f84728de8f7482834de442fd90c2c3d50b", size = 272420, upload-time = "2025-04-15T13:46:13.732Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/99/69/2a29773b43a24ee04eb26af492d85d520b30a86cfef22a0885e77e9c4a16/sentence_transformers-5.0.0.tar.gz", hash = "sha256:e5a411845910275fd166bacb01d28b7f79537d3550628ae42309dbdd3d5670d1", size = 366847, upload-time = "2025-07-01T13:01:33.04Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/45/2d/1151b371f28caae565ad384fdc38198f1165571870217aedda230b9d7497/sentence_transformers-4.1.0-py3-none-any.whl", hash = "sha256:382a7f6be1244a100ce40495fb7523dbe8d71b3c10b299f81e6b735092b3b8ca", size = 345695, upload-time = "2025-04-15T13:46:12.44Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ff/178f08ea5ebc1f9193d9de7f601efe78c01748347875c8438f66f5cecc19/sentence_transformers-5.0.0-py3-none-any.whl", hash = "sha256:346240f9cc6b01af387393f03e103998190dfb0826a399d0c38a81a05c7a5d76", size = 470191, upload-time = "2025-07-01T13:01:31.619Z" },
]
[[package]]
name = "setuptools"
-version = "80.7.1"
+version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9e/8b/dc1773e8e5d07fd27c1632c45c1de856ac3dbf09c0147f782ca6d990cf15/setuptools-80.7.1.tar.gz", hash = "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552", size = 1319188, upload-time = "2025-05-15T02:41:00.955Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/18/0e835c3a557dc5faffc8f91092f62fc337c1dab1066715842e7a4b318ec4/setuptools-80.7.1-py3-none-any.whl", hash = "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009", size = 1200776, upload-time = "2025-05-15T02:40:58.887Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
@@ -4491,7 +4580,7 @@ wheels = [
[[package]]
name = "torch"
-version = "2.7.0"
+version = "2.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -4518,14 +4607,14 @@ dependencies = [
{ name = "typing-extensions" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/14/24/720ea9a66c29151b315ea6ba6f404650834af57a26b2a04af23ec246b2d5/torch-2.7.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:868ccdc11798535b5727509480cd1d86d74220cfdc42842c4617338c1109a205", size = 99015553, upload-time = "2025-04-23T14:34:41.075Z" },
- { url = "https://files.pythonhosted.org/packages/4b/27/285a8cf12bd7cd71f9f211a968516b07dcffed3ef0be585c6e823675ab91/torch-2.7.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b52347118116cf3dff2ab5a3c3dd97c719eb924ac658ca2a7335652076df708", size = 865046389, upload-time = "2025-04-23T14:32:01.16Z" },
- { url = "https://files.pythonhosted.org/packages/74/c8/2ab2b6eadc45554af8768ae99668c5a8a8552e2012c7238ded7e9e4395e1/torch-2.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:434cf3b378340efc87c758f250e884f34460624c0523fe5c9b518d205c91dd1b", size = 212490304, upload-time = "2025-04-23T14:33:57.108Z" },
- { url = "https://files.pythonhosted.org/packages/28/fd/74ba6fde80e2b9eef4237fe668ffae302c76f0e4221759949a632ca13afa/torch-2.7.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:edad98dddd82220465b106506bb91ee5ce32bd075cddbcf2b443dfaa2cbd83bf", size = 68856166, upload-time = "2025-04-23T14:34:04.012Z" },
- { url = "https://files.pythonhosted.org/packages/cb/b4/8df3f9fe6bdf59e56a0e538592c308d18638eb5f5dc4b08d02abb173c9f0/torch-2.7.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2a885fc25afefb6e6eb18a7d1e8bfa01cc153e92271d980a49243b250d5ab6d9", size = 99091348, upload-time = "2025-04-23T14:33:48.975Z" },
- { url = "https://files.pythonhosted.org/packages/9d/f5/0bd30e9da04c3036614aa1b935a9f7e505a9e4f1f731b15e165faf8a4c74/torch-2.7.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:176300ff5bc11a5f5b0784e40bde9e10a35c4ae9609beed96b4aeb46a27f5fae", size = 865104023, upload-time = "2025-04-23T14:30:40.537Z" },
- { url = "https://files.pythonhosted.org/packages/d1/b7/2235d0c3012c596df1c8d39a3f4afc1ee1b6e318d469eda4c8bb68566448/torch-2.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d0ca446a93f474985d81dc866fcc8dccefb9460a29a456f79d99c29a78a66993", size = 212750916, upload-time = "2025-04-23T14:32:22.91Z" },
- { url = "https://files.pythonhosted.org/packages/90/48/7e6477cf40d48cc0a61fa0d41ee9582b9a316b12772fcac17bc1a40178e7/torch-2.7.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:27f5007bdf45f7bb7af7f11d1828d5c2487e030690afb3d89a651fd7036a390e", size = 68575074, upload-time = "2025-04-23T14:32:38.136Z" },
+ { url = "https://files.pythonhosted.org/packages/66/81/e48c9edb655ee8eb8c2a6026abdb6f8d2146abd1f150979ede807bb75dcb/torch-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:03563603d931e70722dce0e11999d53aa80a375a3d78e6b39b9f6805ea0a8d28", size = 98946649, upload-time = "2025-06-04T17:38:43.031Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/24/efe2f520d75274fc06b695c616415a1e8a1021d87a13c68ff9dce733d088/torch-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d632f5417b6980f61404a125b999ca6ebd0b8b4bbdbb5fbbba44374ab619a412", size = 821033192, upload-time = "2025-06-04T17:38:09.146Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d9/9c24d230333ff4e9b6807274f6f8d52a864210b52ec794c5def7925f4495/torch-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:23660443e13995ee93e3d844786701ea4ca69f337027b05182f5ba053ce43b38", size = 216055668, upload-time = "2025-06-04T17:38:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/95/bf/e086ee36ddcef9299f6e708d3b6c8487c1651787bb9ee2939eb2a7f74911/torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0da4f4dba9f65d0d203794e619fe7ca3247a55ffdcbd17ae8fb83c8b2dc9b585", size = 68925988, upload-time = "2025-06-04T17:38:29.273Z" },
+ { url = "https://files.pythonhosted.org/packages/69/6a/67090dcfe1cf9048448b31555af6efb149f7afa0a310a366adbdada32105/torch-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e08d7e6f21a617fe38eeb46dd2213ded43f27c072e9165dc27300c9ef9570934", size = 99028857, upload-time = "2025-06-04T17:37:50.956Z" },
+ { url = "https://files.pythonhosted.org/packages/90/1c/48b988870823d1cc381f15ec4e70ed3d65e043f43f919329b0045ae83529/torch-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:30207f672328a42df4f2174b8f426f354b2baa0b7cca3a0adb3d6ab5daf00dc8", size = 821098066, upload-time = "2025-06-04T17:37:33.939Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/eb/10050d61c9d5140c5dc04a89ed3257ef1a6b93e49dd91b95363d757071e0/torch-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:79042feca1c634aaf6603fe6feea8c6b30dfa140a6bbc0b973e2260c7e79a22e", size = 216336310, upload-time = "2025-06-04T17:36:09.862Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/29/beb45cdf5c4fc3ebe282bf5eafc8dfd925ead7299b3c97491900fe5ed844/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946", size = 68645708, upload-time = "2025-06-04T17:34:39.852Z" },
]
[[package]]
@@ -4542,7 +4631,7 @@ wheels = [
[[package]]
name = "transformers"
-version = "4.51.3"
+version = "4.53.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -4556,21 +4645,21 @@ dependencies = [
{ name = "tokenizers" },
{ name = "tqdm" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266, upload-time = "2025-04-14T08:15:00.485Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/40/f2d2c3bcf5c6135027cab0fd7db52f6149a1c23acc4e45f914c43d362386/transformers-4.53.0.tar.gz", hash = "sha256:f89520011b4a73066fdc7aabfa158317c3934a22e3cd652d7ffbc512c4063841", size = 9177265, upload-time = "2025-06-26T16:10:54.729Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/0c/68d03a38f6ab2ba2b2829eb11b334610dd236e7926787f7656001b68e1f2/transformers-4.53.0-py3-none-any.whl", hash = "sha256:7d8039ff032c01a2d7f8a8fe0066620367003275f023815a966e62203f9f5dd7", size = 10821970, upload-time = "2025-06-26T16:10:51.505Z" },
]
[[package]]
name = "triton"
-version = "3.3.0"
+version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/7d/74/4bf2702b65e93accaa20397b74da46fb7a0356452c1bb94dbabaf0582930/triton-3.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47bc87ad66fa4ef17968299acacecaab71ce40a238890acc6ad197c3abe2b8f1", size = 156516468, upload-time = "2025-04-09T20:27:48.196Z" },
- { url = "https://files.pythonhosted.org/packages/0a/93/f28a696fa750b9b608baa236f8225dd3290e5aff27433b06143adc025961/triton-3.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce4700fc14032af1e049005ae94ba908e71cd6c2df682239aed08e49bc71b742", size = 156580729, upload-time = "2025-04-09T20:27:55.424Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1f/dfb531f90a2d367d914adfee771babbd3f1a5b26c3f5fbc458dee21daa78/triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240", size = 155673035, upload-time = "2025-05-29T23:40:02.468Z" },
+ { url = "https://files.pythonhosted.org/packages/28/71/bd20ffcb7a64c753dc2463489a61bf69d531f308e390ad06390268c4ea04/triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42", size = 155735832, upload-time = "2025-05-29T23:40:10.522Z" },
]
[[package]]