[Refactor] 리팩터링

This commit is contained in:
tv0924@icloud.com 2025-06-27 09:45:50 +09:00
commit bbd2d6d636
20 changed files with 397 additions and 257 deletions

9
lib/agents/__init__.py Normal file
View file

@ -0,0 +1,9 @@
from lib.agents.get_sso_list import get_sso_list
# 버전 업데이트 대비
from lib.agents.get_sso_list_v2 import get_sso_list as get_sso_list_v2
from lib.agents.login_google import login_google
__all__ = [
"get_sso_list",
"login_google",
]

View file

@ -1,41 +0,0 @@
import json
from pydantic import BaseModel
from lib.prompt.get_sso_list import get_sso_list_task
from lib.browser_use_utils.run_task import run_task
NOT_FOUND_LOGIN_PAGE = 0
FOUND_LOGIN_PAGE = 1
class FindLoginPageResponse(BaseModel):
status: int = NOT_FOUND_LOGIN_PAGE # 0 if not found, 1 if found
msg: str | None = None
url: str | None = None
sso_list: list[str] = [] # List of SSO providers found on the login page
async def get_sso_list(target_url) -> tuple[bool, str | FindLoginPageResponse | None]:
task = get_sso_list_task
ReturnModel = FindLoginPageResponse
success, response = await run_task(target_url, ReturnModel, task)
if not success:
return False, response
if isinstance(response, str):
return False, response
if isinstance(response, FindLoginPageResponse):
if response.status == FOUND_LOGIN_PAGE:
if not response.sso_list:
response.msg = "로그인 페이지는 찾았지만 SSO 제공자가 없습니다."
else:
response.msg = "로그인 페이지와 SSO 제공자를 찾았습니다."
else:
response.msg = "로그인 페이지를 찾지 못했습니다."
else:
return False, "응답 형식이 올바르지 않습니다. FindLoginPageResponse가 아닙니다."
return True, response

View file

@ -0,0 +1,3 @@
from lib.agents.get_sso_list.get_sso_list import get_sso_list
__all__ = ["get_sso_list"]

View file

@ -0,0 +1,22 @@
from lib.agents.get_sso_list.prompt import get_sso_list_task, FindLoginPageResponse
from lib.browser_use_utils.run_task import run_task
NOT_FOUND_LOGIN_PAGE = 0
FOUND_LOGIN_PAGE = 1
async def get_sso_list(target_url) -> tuple[bool, str | FindLoginPageResponse | None]:
task = get_sso_list_task
ReturnModel = FindLoginPageResponse
success, response = await run_task(target_url, ReturnModel, task)
if not success:
return False, response
if isinstance(response, str):
return False, response
return True, response

View file

@ -1,3 +1,10 @@
from pydantic import BaseModel
class FindLoginPageResponse(BaseModel):
msg: str | None = None
url: str | None = None
sso_list: list[str] = [] # List of SSO providers found on the login page
get_sso_list_task = """
You are an expert in finding login pages.
@ -9,7 +16,6 @@ Your task is to navigate to the login page of the given URL. Follow the steps be
- If the browser is blocked when trying to access the page due to firewall, CAPTCHA, regional restrictions, or other access denials immediately terminate the process and return the following JSON:
```json
{
"status": 0,
"msg": "Blocked",
"url": "",
"sso_list": []
@ -37,7 +43,6 @@ Your task is to navigate to the login page of the given URL. Follow the steps be
- If the login page is successfully found, return:
```json
{
"status": 1,
"msg": "Login page found",
"url": "https://example.com/login",
"sso_list": ["Google", "GitHub"]
@ -46,7 +51,6 @@ Your task is to navigate to the login page of the given URL. Follow the steps be
- If the login page cannot be found, return:
```json
{
"status": 0,
"msg": "Login page not found",
"url": "",
"sso_list": []
@ -55,7 +59,6 @@ Your task is to navigate to the login page of the given URL. Follow the steps be
- If blocked (as in step 0), return:
```json
{
"status": 0,
"msg": "Blocked",
"url": "",
"sso_list": []

View file

@ -0,0 +1,3 @@
from lib.agents.get_sso_list_v2 import get_sso_list
__all__ = ["get_sso_list"]

View file

@ -0,0 +1,20 @@
from lib.agents.get_sso_list_v2.prompt import get_sso_list_task, FindLoginPageResponse
from lib.browser_use_utils.run_task import run_task
# TODO - Split find login page agent and get SSO list agent
async def get_sso_list(target_url) -> tuple[bool, str | FindLoginPageResponse | None]:
task = get_sso_list_task
ReturnModel = FindLoginPageResponse
success, response = await run_task(target_url, ReturnModel, task)
if not success:
return False, response
if isinstance(response, str):
return False, response
return True, response

View file

@ -0,0 +1,68 @@
from pydantic import BaseModel
class FindLoginPageResponse(BaseModel):
msg: str | None = None
url: str | None = None
sso_list: list[str] = [] # List of SSO providers found on the login page
get_sso_list_task = """
You are an expert in finding login pages.
Your task is to navigate to the login page of the given URL. Follow the steps below strictly and return results only in the specified format.
You are NOT allowed to navigate to URLs that are not directly discoverable within the initial domain. Do NOT use search engines or guess external login URLs.
0. INITIAL BLOCK CHECK
- If the browser is blocked when trying to access the page due to firewall, CAPTCHA, regional restrictions, or other access denials immediately terminate the process and return the following JSON:
```json
{
"msg": "Blocked",
"url": "",
"sso_list": []
}
```
- Do NOT proceed to further steps in this case.
1. LOGIN PAGE NAVIGATION
- Navigate only to a **client-side (non-enterprise)** login page within the provided domain.
- Do NOT rely on external tools, search engines, or links not directly found on the site.
- If a consent popup (e.g. for privacy/cookies) appears, you MUST dismiss or close it before proceeding.
- Since step 0 confirmed access, assume the page now loads properly.
2. SSO BUTTON IDENTIFICATION
- On the login page, look for the following social login (SSO) buttons:
- Google, GitHub, Facebook, LinkedIn, Microsoft, Naver, Slack, Etc.
- Proceed only if it is clearly an **actual SSO button**.
- Exclude the following:
- Passkey-related buttons
- Username/password fields
- Email-based login
- Non-OAuth methods such as certificate or phone verification
3. RETURN FORMAT
- If the login page is successfully found, return:
```json
{
"msg": "Login page found",
"url": "https://example.com/login",
"sso_list": ["Google", "GitHub"]
}
```
- If the login page cannot be found, return:
```json
{
"msg": "Login page not found",
"url": "",
"sso_list": []
}
```
- If blocked (as in step 0), return:
```json
{
"msg": "Blocked",
"url": "",
"sso_list": []
}
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
"""

View file

@ -0,0 +1,3 @@
from lib.agents.login_google.login_google import login_google
__all__ = ["login_google"]

View file

@ -0,0 +1,11 @@
from lib.agents.login_google.prompt import login_google_task, LoginGoogleResponse
from lib.browser_use_utils.run_task import run_task
async def login_google(target_url) -> tuple[bool, str | LoginGoogleResponse | None]:
task = login_google_task
ReturnModel = LoginGoogleResponse
success, response = await run_task(target_url, ReturnModel, task)
if not success:
return False, None
return True, response

View file

@ -0,0 +1,60 @@
from pydantic import BaseModel
from lib.config import GOOGLE_ID, GOOGLE_PASSWORD
class LoginGoogleResponse(BaseModel):
msg: str | None = None
status: str | None = None # "success", "mfa_required", "google_blocked", "sso_not_found", "login_page_not_found", "invalid_credentials"
final_url: str | None = None
login_google_task = f"""
You are a web automation agent.
Your task is to visit the given domain and perform a full login via the **Google SSO button**, following all steps strictly as described below.
Target: Find a login page inside this domain that allows "Sign in with Google", and use it to complete login via Google.
Instructions:
1. If any cookie or privacy popups appear, dismiss or accept them.
2. Navigate through the site's UI to find the **login or sign-in page** (e.g., via buttons like "Log In", "Sign In", "Get Started").
- Only follow links within the same domain.
3. On the login page, look for a clearly labeled **Google SSO button** typically labeled as:
- "Continue with Google"
- "Sign in with Google"
- or a button with the Google 'G' icon
4. Click the **Google login button**.
- The Google login flow MUST open in a **new browser tab** (not a new window or popup).
- If the login opens in a new **window** or **popup**, do NOT continue. Immediately stop and return the appropriate status.
5. Check if the user is **already logged in to Google and immediately redirected back to the original site** without showing a Google login screen.
- If so, treat the login as successful and return immediately.
6. If redirected to the Google login page:
- If a **CAPTCHA**, **MFA prompt**, or a request for **ID/password entry** appears, do NOT proceed.
- Immediately stop and return the appropriate status.
7. If login proceeds without interruptions, wait for redirection back to the original site and record the final URL.
Credentials to use (only if needed):
- Email: {GOOGLE_ID}
- Password: {GOOGLE_PASSWORD}
Constraints:
- Do NOT use search engines or guess URLs only navigate links discoverable from https://example.com
- Do NOT use autofill, saved sessions, or cookies.
- Do NOT proceed with login if:
- The login opens in a new window (only tabs are allowed)
- CAPTCHA or MFA appears
- ID/password input is required
- If the user is already logged in to Google and redirected back automatically, stop there and report success.
Final Output:
Return the result in the following format only:
```json
{{
"msg": "Google login completed",
"status": "success" | "already_logged_in" | "mfa_required" | "captcha_triggered" | "window_blocked" | "idpw_required" | "google_blocked" | "sso_not_found" | "login_page_not_found",
"final_url": "<url_after_login_redirect or empty string>"
}}
```
- Return ONLY the JSON object. Do NOT include any explanation, logging, or extra output.
"""

View file

@ -0,0 +1,15 @@
from lib.browser_use_utils.clean_resources import clean_resources, clean_agent_resources, clean_session_resources
from lib.browser_use_utils.create_google_ai import create_google_ai
from lib.browser_use_utils.get_profile import get_profile
from lib.browser_use_utils.run_agent import run_agent
from lib.browser_use_utils.run_task import run_task
__all__ = [
"clean_resources",
"clean_agent_resources",
"clean_session_resources",
"create_google_ai",
"get_profile",
"run_agent",
"run_task",
]

View file

@ -0,0 +1,40 @@
from typing import Any
from pydantic import BaseModel
from lib.browser_use_utils.clean_resources import clean_agent_resources
from lib.config import GOOGLE_MODEL
from browser_use import (
Agent,
Controller,
)
from lib.browser_use_utils.create_google_ai import create_google_ai
async def run_agent(session, initial_actions, ReturnModel: type[BaseModel], task: str) -> tuple[bool, str, Any | None]:
controller = Controller(output_model=ReturnModel, exclude_actions=['search_google'])
agent = Agent(
browser_session=session,
initial_actions=initial_actions,
task=task,
llm=create_google_ai(GOOGLE_MODEL),
controller=controller,
)
try:
response = await agent.run()
final_result = response.final_result()
if final_result is None:
return False, "LLM이 반환한 최종 결과가 없습니다.", None
except Exception as e:
# API 쿼터 문제인지 확인
if "ResourceExhausted" in str(e) or "429" in str(e):
return False, "API 쿼터 에러로 인한 실패", None
# 일반 에러 처리
else:
return False, "일반 에러로 인한 실패", None
finally:
await clean_agent_resources(agent)
return True, "ok", final_result

View file

@ -2,20 +2,14 @@ import json
from typing import Any
from pydantic import BaseModel
from browser_use import (
Agent,
Controller,
BrowserSession
)
from patchright.async_api import async_playwright as async_patchright
from lib.utils.logger import logger
from lib.prompt.get_sso_list import get_sso_list_task
from lib.browser_use_utils.create_google_ai import create_google_ai
from lib.browser_use_utils.get_profile import get_profile
from lib.browser_use_utils.clean_resources import clean_session_resources, clean_agent_resources
from lib.config import GOOGLE_MODEL
from lib.browser_use_utils import get_profile, clean_session_resources, run_agent
async def run_task(target_url: str, ReturnModel: type[BaseModel], task: str) -> tuple[bool, str | Any | None]:
async def run_task(target_url: str, ReturnModel: type[BaseModel], task: str) -> tuple[bool, type[BaseModel] | None]:
session = BrowserSession(
playwright=(await async_patchright().start()),
browser_profile=await get_profile(),
@ -23,36 +17,15 @@ async def run_task(target_url: str, ReturnModel: type[BaseModel], task: str) ->
initial_actions = [{"open_tab": {"url": target_url}}]
controller = Controller(output_model=ReturnModel, exclude_actions=['search_google'])
agent = Agent(
browser_session=session,
seccess, msg, final_result = await run_agent(session=session,
initial_actions=initial_actions,
task=task,
llm=create_google_ai(GOOGLE_MODEL),
controller=controller,
)
try:
response = await agent.run()
final_result = response.final_result()
if final_result is None:
logger(f"⚠️ 최종 결과가 없습니다. 에이전트 실행 실패: {target_url}")
print(f"⚠️ 최종 결과가 없습니다. 에이전트 실행 실패: {target_url}")
return False, "최종 결과가 없습니다. 에이전트 실행 실패"
except Exception as e:
# API 쿼터 문제인지 확인
if "ResourceExhausted" in str(e) or "429" in str(e):
logger(f"⚠️ API 쿼터 에러로 인한 실패: {target_url} | {e}")
print(f"⚠️ API 쿼터 에러로 인한 실패: {target_url} | {e}")
return False, "API 쿼터 에러로 인한 실패"
# 일반 에러 처리
else:
logger(f"⚠️ 일반 에러로 인한 실패: {target_url} | {e}")
print(f"⚠️ 일반 에러로 인한 실패: {target_url} | {e}")
return False, "일반 에러로 인한 실패"
finally:
await clean_agent_resources(agent)
ReturnModel=ReturnModel,
task=task)
if not seccess:
logger(f"⚠️ LLM 실행 실패: {target_url} | {msg}")
print(f"⚠️ LLM 실행 실패: {target_url} | {msg}")
await clean_session_resources(session)
return False, None
try:
data = json.loads(final_result)
@ -61,7 +34,7 @@ async def run_task(target_url: str, ReturnModel: type[BaseModel], task: str) ->
except Exception as e:
logger(f"⚠️ LLM 응답 결과 파싱 실패: {target_url} | {e}\n원본 결과: {data.msg}")
print(f"⚠️ LLM 응답 결과 파싱 실패: {target_url} | {e}\n원본 결과: {data.msg}")
return False, "LLM 응답 결과 파싱 실패"
return False, None
finally:
await clean_session_resources(session)

View file

@ -4,5 +4,7 @@ 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-preview-05-20")
GOOGLE_PLANNER_MODEL = os.getenv("GOOGLE_PLANNER_MODEL", "gemini-2.5-pro-preview-06-05")
GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.5-flash")
GOOGLE_ID = os.getenv("GOOGLE_ID", "google")
GOOGLE_PASSWORD = os.getenv("GOOGLE_PASSWORD", "google")

18
lib/utils/__init__.py Normal file
View file

@ -0,0 +1,18 @@
from lib.utils.env_checker import check_env_variables
from lib.utils.is_html import is_html_url
from lib.utils.logger import logger
from lib.utils.notify_backend import notify_backend
from lib.utils.progress_checker import save_progress, load_progress
from lib.utils.read_txt import read_lines_between
from lib.utils.save_oauth_providers import save_oauth_providers
__all__ = [
"check_env_variables",
"is_html_url",
"logger",
"notify_backend",
"read_lines_between",
"save_progress",
"load_progress",
"save_oauth_providers",
]

View file

@ -1,90 +0,0 @@
from dotenv import load_dotenv
import os
load_dotenv()
google_id = os.getenv("GOOGLE_ID")
google_password = os.getenv("GOOGLE_PASSWORD")
naver_id = os.getenv("NAVER_ID")
naver_password = os.getenv("NAVER_PASSWORD")
facebook_id = os.getenv("FACEBOOK_ID")
facebook_password = os.getenv("FACEBOOK_PASSWORD")
github_id = os.getenv("GITHUB_ID")
github_password = os.getenv("GITHUB_PASSWORD")
# Extended planner prompt
extend_planner_system_message = f"""
🎯 Mission: Collect Initial SSO Redirect URLs (For Browser Automation)
**모든 STEP에서 구글 검색, Bing 검색 어떤 외부 검색 기능도 절대 사용하지 않고, 초기에 주어진 URL에서 탐색하세요.**
**초기에 주어진 URL 내에서 실제로 확인되지 않은 URL로 직접 이동하는것은 허용되지 않습니다.**
0. **초기 블록(Block) 체크**
- 브라우저가 로그인 페이지에 접근하려 , **페이지가 차단(blocked)** 되거나 **방화벽, CAPTCHA, 접근 제한** 등으로 인해 정상적으로 로드되지 않으면 즉시 프로세스를 종료하고 아래 JSON만 반환해야 합니다.
```json
[
{{
"provider": "Blocked",
"oauth_uri": "-"
}}
]
```
- 이후 단계로 절대 넘어가지 않도록 합니다.
1. **로그인 페이지 탐색**
- **클라이언트(비엔터프라이즈) 로그인 페이지** 직접 이동합니다. **검색 엔진을 사용하여 찾아서는 됩니다.**
- 접근 **개인정보/쿠키/동의 팝업** 뜨면, 이를 반드시 **닫거나(Dismiss)** 처리하고 계속 진행합니다.
- (이미 0단계에서 블록 여부를 확인했으므로, 단계에서는 페이지가 정상 로드되었다고 가정합니다.)
2. **SSO 버튼 식별**
- 로그인 페이지에서 다음과 같은 소셜 로그인 버튼을 찾습니다:
- Google, GitHub, Facebook, Linkedin, Microsoft, Naver
- **실제 SSO 버튼**임이 명확히 확인되는 경우에만 진행합니다.
- 제외 대상:
- Passkey 관련 버튼
- 아이디/비밀번호 입력란
- 이메일 기반 로그인
- 인증서, 휴대폰 인증 -OAuth 로그인 옵션
3. **SSO 버튼 클릭 로그인 시도**
- 유효한 SSO 버튼이 발견되면, 버튼을 클릭합니다.
- 클릭 ** 번째로 리디렉션된 URL(쿼리 스트링 포함)** `oauth_uri` 기록합니다.
- 공급자 페이지가 열리면, 아래 자격증명을 이용해 로그인을 시도합니다, 아래 자격증명에 포함되지 않는 SSO 버튼도 클릭까지는 시도합니다.:
- Google `{google_id}` / `{google_password}`
- Naver `{naver_id}` / `{naver_password}`
- GitHub `{github_id}` / `{github_password}`
- facebook `{facebook_id}` / `{facebook_password}`
- **자격증명이 주어진 SSO 버튼인 경우 로그인 과정을 진행합니다.**
- 로그인 과정이 모두 끝나거나 로그인이 되지 않는 경우 세션 쿠키를 모두 삭제하고 페이지를 새로고침합니다.
- 한번이라도 SSO 버튼을 클릭한 경우, 해당 버튼은 이상 탐색하지 않습니다.
- id/pw 입력 성공 , 아직 로그인되지 않았다면, 최대 5초간 대기합니다.
- 아직 로그인을 시도하지 않은 SSO 버튼이 있다면 이전 단계인 1. **로그인 페이지 탐색**, 2. **SSO 버튼 식별**, 3. **SSO 버튼 클릭 로그인 시도** 돌아가 절차를 반복합니다.
- 최종 결과는 다음과 같이 기록합니다:
```json
[
{{
"provider": "Google",
"oauth_uri": "(optional) https://example.com/auth/google?client_id=...",
}},
{{
"provider": "Naver",
"oauth_uri": "(optional) https://example.com/auth/naver?client_id=...",
}}
]
```
4. **SSO 버튼 미발견 또는 오류 발생 **
- 페이지 내부에 유효한 SSO 버튼이 전혀 없거나, 탐색 예기치 않은 오류가 발생하면 즉시 프로세스를 종료하고 ** 배열** 반환합니다:
```json
[]
```
5. **중요 사항**
- **반드시** 위의 단계들을 순서대로 수행해야 하며, 단계에서 발생하는 예외 상황을 정확히 처리해야 합니다.
- **반복 행동** 감지되면 즉시 배열을 반환하고, **블록된 페이지** 초기 단계에서 처리하여 프로세스를 종료해야 합니다.
- **SSO 버튼이 발견되지 않거나, 오류가 발생한 경우에도 배열을 반환해야 합니다.**
- **반드시** JSON 형식으로 결과를 반환해야 하며, 다른 형식은 허용되지 않습니다.
- 최대한 효율적인 단계로 진행하며, 불필요한 반복이나 검색 엔진 사용을 피해야 합니다.
"""

35
main.py
View file

@ -3,18 +3,18 @@ import argparse
import signal
from dotenv import load_dotenv
from lib.config import BACKEND_URL
from lib.utils.notify_backend import notify_backend
from lib.utils.is_html import is_html_url
from lib.utils.read_txt import read_lines_between
from lib.utils.progress_checker import save_progress, load_progress
from lib.utils.env_checker import check_env_variables
from lib.agents.get_sso_list import get_sso_list
from lib.utils import notify_backend, is_html_url, read_lines_between, save_progress, load_progress, check_env_variables
from lib.agents import get_sso_list, login_google
load_dotenv()
check_env_variables()
backend_url = BACKEND_URL
login_agents = {
"google": login_google
}
# ── URL별로 Browser를 새로 띄우는 함수 ──
async def scan_one_url(url: str, skip_html_check: bool = False):
target_url = url if url.startswith("http") else f"https://{url}"
@ -28,7 +28,28 @@ async def scan_one_url(url: str, skip_html_check: bool = False):
# Backend에 스캔 시작 알림
notify_backend(target_url)
print(await get_sso_list(target_url))
success, response = await get_sso_list(target_url)
if not success:
return
if len(response.sso_list) == 0:
return
for sso in response.sso_list:
target_login_agent = login_agents.get(sso.lower())
if target_login_agent:
print(f"🔍 {target_url} 에서 SSO 발견: {sso}, 로그인 시도 중...")
success, login_response = await target_login_agent(target_url)
if not success:
print(f"⚠️ {target_url} 에서 {sso} 로그인 실패")
continue
print(f"{target_url} 에서 {sso} 로그인 성공: {login_response.final_url}")
else:
print(f"{target_url} 에서 SSO 발견: {sso} | TODO")
# Backend에 스캔 완료 알림
# 오탐 검증

View file

@ -5,6 +5,6 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"browser-use[memory]==0.3.2",
"browser-use[memory]==0.2.7",
"patchright==1.52.5",
]

146
uv.lock generated
View file

@ -51,18 +51,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
[[package]]
name = "authlib"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/9d/b1e08d36899c12c8b894a44a5583ee157789f26fc4b176f8e4b6217b56e1/authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210", size = 158371, upload-time = "2025-05-23T00:21:45.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981, upload-time = "2025-05-23T00:21:43.075Z" },
]
[[package]]
name = "backoff"
version = "2.2.1"
@ -85,19 +73,47 @@ 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 = "boto3"
version = "1.38.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/ea50e25a049072c0078045437d25fc9c8eaec4bd58f2cc340e6ed52e55cd/boto3-1.38.44.tar.gz", hash = "sha256:af1769dfb2a8a30eec24d0b74a8c17db2accc5a6224d4fab39dd36df6590f741", size = 111899, upload-time = "2025-06-25T19:27:40.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/73/4a1bbd696e492f17064e7404c49d4d3bafcc8b50239ec6624c10ea824dd1/boto3-1.38.44-py3-none-any.whl", hash = "sha256:73fcb2f8c7bec25d17e3f1940a1776c515b458b3da77ad3a31a177479591028b", size = 139923, upload-time = "2025-06-25T19:27:38.748Z" },
]
[[package]]
name = "botocore"
version = "1.38.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/06/c6e652e8b449837218d83cedda9c54104cfd5d38dc97762044a40116b209/botocore-1.38.44.tar.gz", hash = "sha256:8d54795a084204e4cd7885d9307e4bfaccc96411dc0384f6ba240b515c45bf54", size = 14050056, upload-time = "2025-06-25T19:27:29.354Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/85/e3cd7bf4237af134a90290c8e37bf7f786c5e58b9ff98eeb0495615e3985/botocore-1.38.44-py3-none-any.whl", hash = "sha256:d0171ac6ec0bfdf86083b41c801f212e2b2d5756a61ea1d45af2051f21dbf886", size = 13710700, upload-time = "2025-06-25T19:27:23.645Z" },
]
[[package]]
name = "browser-use"
version = "0.3.2"
version = "0.2.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
{ name = "anyio" },
{ name = "authlib" },
{ name = "bubus" },
{ name = "faiss-cpu" },
{ name = "google-api-core" },
{ name = "httpx" },
{ name = "langchain" },
{ name = "langchain-anthropic" },
{ name = "langchain-aws" },
{ name = "langchain-core" },
{ name = "langchain-deepseek" },
{ name = "langchain-google-genai" },
@ -118,14 +134,13 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid7" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/b0620dea406c878923b38dab4c9391d822a854a7053ec3ba2c831a8f8da1/browser_use-0.3.2.tar.gz", hash = "sha256:600881d087ef246d10505aa133cc18f7ac2f3f8ddcb6210c00a8cabf0a4b9aa1", size = 175299, upload-time = "2025-06-22T05:26:20.979Z" }
sdist = { url = "https://files.pythonhosted.org/packages/42/86/8d25175730145a8f94715e5ceb3e050d8221fc81d7dee8c8f18ddf4206a3/browser_use-0.2.7.tar.gz", hash = "sha256:a2e0b0eb34e6fb5ef46e4e10ad0b4a42854fc2445d3e53b3ba393b9295019725", size = 155467, upload-time = "2025-06-14T08:55:54.739Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/04/0446df95b031362fbbbd20e4b389d4dee98f27d2c40ed38d792842bcc807/browser_use-0.3.2-py3-none-any.whl", hash = "sha256:500340bd3d41440072d9845c640b0cb5decfacc423459ab7a28ce9273b1b1601", size = 195490, upload-time = "2025-06-22T05:26:19.545Z" },
{ url = "https://files.pythonhosted.org/packages/e7/93/2305e33ca4470abafd087be820256bd96c495ab685425582d811bb22837a/browser_use-0.2.7-py3-none-any.whl", hash = "sha256:bc534a369ef85ff3905abae05dab1d4a996676ad285a3dff9aa6c5211854872d", size = 172584, upload-time = "2025-06-14T08:55:53.355Z" },
]
[package.optional-dependencies]
memory = [
{ name = "faiss-cpu" },
{ name = "sentence-transformers" },
]
@ -140,26 +155,10 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "browser-use", extras = ["memory"], specifier = "==0.3.2" },
{ name = "browser-use", extras = ["memory"], specifier = "==0.2.7" },
{ name = "patchright", specifier = "==1.52.5" },
]
[[package]]
name = "bubus"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
{ name = "anyio" },
{ name = "pydantic" },
{ name = "typing-extensions" },
{ name = "uuid7" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/29/27666c76a6187847c9436e6a0c478ea18a9df5356cbd43dd54ebcd37da10/bubus-1.2.1.tar.gz", hash = "sha256:8ebbaa8313affa39b53106d864f633b51655cde42efb2282aa3166e9fe5f0322", size = 26028, upload-time = "2025-06-24T06:13:21.568Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/e8/ce0e5996bf4f4cccb6fab5cb468b283085d4c1711e41dc1d1db3361f9f71/bubus-1.2.1-py3-none-any.whl", hash = "sha256:72ad267758c6938336a6ff6017eee47ed864a95ffb7f4d0296f7b60f5520c2b9", size = 27660, upload-time = "2025-06-24T06:13:20.753Z" },
]
[[package]]
name = "cachetools"
version = "5.5.2"
@ -231,41 +230,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cryptography"
version = "45.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" },
{ url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" },
{ url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" },
{ url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" },
{ url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" },
{ url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" },
{ url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" },
{ url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" },
{ url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" },
{ url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" },
{ url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" },
{ url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" },
{ url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" },
{ url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" },
{ url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" },
{ url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" },
{ url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" },
{ url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" },
{ url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" },
{ url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" },
{ url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" },
]
[[package]]
name = "cython"
version = "3.1.2"
@ -613,6 +577,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
]
[[package]]
name = "jmespath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
]
[[package]]
name = "joblib"
version = "1.5.1"
@ -675,6 +648,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/c0/9a1d58ab8718505bf25b7ad375a2a104886dfe64519d8b96442bb295637e/langchain_anthropic-0.3.15-py3-none-any.whl", hash = "sha256:894d670bc44e68e0b1f2f09e7e7f977a8f07085a596f114c79aefbb789f6d88d", size = 28054, upload-time = "2025-06-03T15:04:43.108Z" },
]
[[package]]
name = "langchain-aws"
version = "0.2.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "boto3" },
{ name = "langchain-core" },
{ name = "numpy" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/84/fc2881c6d67be297cccd81982dfc16c9b3996b4112145a7a6de6e0f28872/langchain_aws-0.2.25.tar.gz", hash = "sha256:80754c7508c9e7771f5e97e46a40e3f41a33c4839d780acc92e75a64950165f0", size = 99141, upload-time = "2025-06-10T20:34:53.417Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/7f/0d9b7eda3ab0426244c2913bf111127ac329c93eff155217415fd2e5ca00/langchain_aws-0.2.25-py3-none-any.whl", hash = "sha256:60132f53ab57bf1ce0f606abfef8a41bbbd170ac6019754dc2f5463650f56f79", size = 120993, upload-time = "2025-06-10T20:34:51.906Z" },
]
[[package]]
name = "langchain-core"
version = "0.3.64"
@ -821,7 +809,7 @@ wheels = [
[[package]]
name = "mem0ai"
version = "0.1.110"
version = "0.1.111"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "openai" },
@ -831,9 +819,9 @@ dependencies = [
{ name = "qdrant-client" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/fd/95c6285ad55a5fb78df17f15b5710273d59ae687c6ff79dcd03acb15e24f/mem0ai-0.1.110.tar.gz", hash = "sha256:8a9b6f45c2c4e5d97ce1aa096dc85991cd657acccde796422b65a52089ca7fcb", size = 107869, upload-time = "2025-06-20T15:01:56.754Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/93/ff302f96e02b5ac80a1ad18b94617985296f78aee212f86d83cba1c2a1a5/mem0ai-0.1.111.tar.gz", hash = "sha256:cc4b1a20cd4fd3b980cca4fd9f77ee4c9cff81b92e6f4d30014fd900dce59bba", size = 108299, upload-time = "2025-06-23T16:23:19.642Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/d6/3d67909445682f5e73e910b187fc64ff84643709ac1956240d8a3834b1bd/mem0ai-0.1.110-py3-none-any.whl", hash = "sha256:4f69df6e633200b9d1b0177f82eaa96bf70a446aee8f40e56eedb67403f14395", size = 166820, upload-time = "2025-06-20T15:01:54.864Z" },
{ url = "https://files.pythonhosted.org/packages/2a/f5/185c88df177d0d9ae1226cc1ae75a2b2480280521a5c7690f1ca6a54b6af/mem0ai-0.1.111-py3-none-any.whl", hash = "sha256:53e8ce3551ffe1454b6e28ba90a8a88907280a9052edfeb872241662a4707f14", size = 168161, upload-time = "2025-06-23T16:23:18.146Z" },
]
[[package]]
@ -4010,6 +3998,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "s3transfer"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" },
]
[[package]]
name = "safetensors"
version = "0.5.3"