diff --git a/README.md b/README.md index 8d8cec8..638e9f2 100644 --- a/README.md +++ b/README.md @@ -116,23 +116,30 @@ uv run run.py 1 100 --skh ## 1. 파일 생성 -`lib/llm/prompt` 폴더로 +`lib/llm/prompt` 폴더에서 fallback 폴더를 복사하여 -![](./docs/list.png) +원하는 프로바이더를 추가해줍니다. `ex) lib/llm/prompt/Google/` -fallback.py를 복사하여 +## 2. prompt.py 수정 -원하는 프로바이더를 추가해줍니다. `ex) lib/llm/prompt/Google.py` +Prompt에서 추가한 파일을 prompt.py에서 수정합니다. -## 2. __init__.py 수정 +만약 로그인 정보를 넣고 싶다면 Sensitive +`Log into example.com as user x_username with password x_password` -![](./docs/guide.png) +## 3. model.py -Prompt에서 추가한 파일을 __init__.py에서 import합니다. +응답할 때 원하는 리턴 값을 `dict`로 받습니다. -## 3. 파일 수정 +## 4. \_\_init\_\_.py 수정 +![alt text](./docs/guide_0.png) -생성한 파일에서 프롬프트를 수정합니다. +추가한 prompt에 따라 import합니다. + +## 5. 사용 방법 +```py +from lib.llm.prompt.fallback import prompt, model +``` # 참고하면 좋을만한 것 diff --git a/docs/guide.png b/docs/guide.png deleted file mode 100644 index 0ddc9e7..0000000 Binary files a/docs/guide.png and /dev/null differ diff --git a/docs/guide_0.png b/docs/guide_0.png new file mode 100644 index 0000000..9dc3f75 Binary files /dev/null and b/docs/guide_0.png differ diff --git a/src/lib/browser_use/agents.py b/src/lib/browser_use/agents.py index 9711e3c..74211f8 100644 --- a/src/lib/browser_use/agents.py +++ b/src/lib/browser_use/agents.py @@ -15,7 +15,6 @@ from lib.utils import ( config, ) from lib.llm import CreateChatGoogleGenerativeAI, get_prompt -import lib.browser_use.model as model # Exponential backoff settings INITIAL_BACKOFF = int(os.getenv("INITIAL_BACKOFF", "60")) # seconds @@ -77,6 +76,7 @@ async def extract_oauth_list(url: str): """첫 번째 Agent: 로그인 페이지를 찾고 OAuth 리스트만 추출""" target_url = url if url.startswith("http") else f"https://{url}" print(f"🔎 OAuth 리스트 추출 시작: {target_url}") + prompt, model = get_prompt("auth") agent_config = { "url": target_url, @@ -97,10 +97,10 @@ async def extract_oauth_list(url: str): else None ), "controller": Controller( - output_model=model.OAuthList, + output_model=model if not isinstance(model, str) else None, exclude_actions=["search_google", "unknown_action", "unkown"], ), - "extend_planner_system_message": get_prompt("auth"), + "extend_planner_system_message": prompt, } } @@ -117,7 +117,12 @@ async def extract_oauth_list(url: str): try: data = json.loads(final_result) oauth_providers = data.get("oauth_providers", []) - return [model.OAuth(provider=provider) for provider in oauth_providers] + if not oauth_providers: + print("❌ OAuth 제공자가 없습니다.") + logger(f"❌ {url} - OAuth 제공자 없음: {final_result}") + return [] + print(f"✅ OAuth 제공자 추출 완료: {oauth_providers}") + return oauth_providers except (json.JSONDecodeError, KeyError) as e: print(f"❌ 결과 파싱 실패: {e}") logger(f"❌ {url} 결과 파싱 실패: {final_result}") @@ -129,6 +134,8 @@ async def test_oauth_login(url: str, oauth_provider: str): target_url = url if url.startswith("http") else f"https://{url}" print(f"🔐 {oauth_provider} 로그인 시작: {target_url}") + prompt, model = get_prompt(oauth_provider) + agent_config = { "url": target_url, "log_context": f"{oauth_provider} 로그인", @@ -149,9 +156,10 @@ async def test_oauth_login(url: str, oauth_provider: str): else None ), "controller": Controller( + output_model=model if not isinstance(model, str) else None, exclude_actions=["search_google", "unknown_action", "unkown"], ), - "extend_planner_system_message": get_prompt(oauth_provider), + "extend_planner_system_message": prompt, } } diff --git a/src/lib/llm/prompt/__init__.py b/src/lib/llm/prompt/__init__.py index 5fa38d2..009f262 100644 --- a/src/lib/llm/prompt/__init__.py +++ b/src/lib/llm/prompt/__init__.py @@ -1,18 +1,17 @@ -# why this is isn't index -# 이 파일을 __init__.py로 만든 이유는 -# 굳이 이 짧은 코드를 파일을 하나 더 만드는게 코드의 가독성을 떨어뜨린다고 판단했기 때문입니다. +from typing import Union, Type +from pydantic import BaseModel -def get_prompt(type:str) -> str: +def get_prompt(type: str) -> tuple[str, Type[BaseModel]] | str: """ Prompt를 반환합니다. :param type: 'auth' {Auth List} 또는 'google' {OAuth Provider}, 'meta' {OAuth Provider}을 지정합니다. - :return: 해당하는 프롬프트 문자열 + :return: 해당하는 프롬프트 문자열 또는 (프롬프트, 모델) 튜플 """ if type.lower() == "auth": - from lib.llm.prompt.auth_list import extract_oauth_list_prompt - return extract_oauth_list_prompt - + from lib.llm.prompt.get_oauth import prompt, model + return prompt, model + else: - from lib.llm.prompt.fallback import extend_planner_system_message - return extend_planner_system_message + from lib.llm.prompt.fallback import model, prompt + return prompt, model diff --git a/src/lib/llm/prompt/auth_list.py b/src/lib/llm/prompt/auth_list.py deleted file mode 100644 index bea1212..0000000 --- a/src/lib/llm/prompt/auth_list.py +++ /dev/null @@ -1,41 +0,0 @@ -# @file purpose: This file contains the prompt for extracting a list of OAuth providers from a web page. -# OAuth 리스트 추출용 프롬프트 (클릭하지 않고 단순 식별만) -extract_oauth_list_prompt = f""" -🎯 목적: 주어진 초기 URL 내에서 **OAuth 로그인 Provider**를 찾아 아래 형식의 JSON으로 정리합니다. - -📌 작업 목표: -- Google, GitHub, Discord, Facebook, Apple, Microsoft, Twitter, LinkedIn 등 **OAuth 인증을 사용하는 외부 로그인 링크**에서 Provider 이름만 모두 수집합니다. -- 로그인 버튼, 링크 클릭 등을 통해 탐색을 진행할 수 있습니다. -- **같은 provider가 여러 번 나와도 하나만 저장**합니다. - -🛑 제한 사항: -- ❌ 로그인 입력창이나 이메일/비밀번호 입력 방식은 제외합니다. -- ❌ 검색 엔진, 사이트 외부 탐색은 금지합니다. -- ❌ URL 추측이나 직접 입력은 금지합니다. -- ❌ OAuth가 없는 경우 빈 배열 `[]`로 반환합니다. -- ❌ OAuth가 아닌 일반 로그인은 무시합니다. - -🔍 탐색 방법: -1. 초기 URL에 접속하여 **클라이언트용 로그인 페이지**로 진입합니다. -2. 페이지가 정상적으로 로드되었다고 가정합니다. -3. 'Continue with X', 'Continue with Google'... 등의 버튼이나 링크를 식별합니다. - - -🧾 출력 형식 (예시): - -```json -{{ - "oauth_providers": [ - "Google", - "GitHub", - "Discord" - ] -}} -``` - -📌 주의: - 결과가 없는 경우 빈 배열 `[]`로 반환합니다. - 정확한 provider 이름을 포함해 주세요. - -""" - diff --git a/src/lib/llm/prompt/fallback/__init__.py b/src/lib/llm/prompt/fallback/__init__.py new file mode 100644 index 0000000..c9a486e --- /dev/null +++ b/src/lib/llm/prompt/fallback/__init__.py @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..d1322ba --- /dev/null +++ b/src/lib/llm/prompt/fallback/model.py @@ -0,0 +1,6 @@ +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" + final_url: str | None = None diff --git a/src/lib/llm/prompt/fallback.py b/src/lib/llm/prompt/fallback/prompt.py similarity index 90% rename from src/lib/llm/prompt/fallback.py rename to src/lib/llm/prompt/fallback/prompt.py index d6954b3..87ad5ce 100644 --- a/src/lib/llm/prompt/fallback.py +++ b/src/lib/llm/prompt/fallback/prompt.py @@ -1,5 +1,5 @@ # Extended planner prompt -extend_planner_system_message = f""" +prompt = f""" 🎯 목적: 웹 자동화를 위한 **SSO 로그인 리디렉션 URL 수집** 📌 주의사항 (전제 조건) @@ -96,6 +96,20 @@ chrome://settings/clearBrowserData에 들어가서 삭제해주세요. --- + +최종 반환: +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": "" +}} +``` + +--- + ## 📎 중요 규칙 요약 * ✅ **모든 SSO 로그인은 반드시 실행** (가능한 버튼은 모두 클릭) 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..1759f89 --- /dev/null +++ b/src/lib/llm/prompt/get_oauth/__init__.py @@ -0,0 +1,2 @@ +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/get_oauth/model.py b/src/lib/llm/prompt/get_oauth/model.py new file mode 100644 index 0000000..b8980d3 --- /dev/null +++ b/src/lib/llm/prompt/get_oauth/model.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +class model(BaseModel): + msg: str | None = None + url: str | None = None + sso_list: list[str] = [] # List of SSO providers found on the login page diff --git a/src/lib/llm/prompt/get_oauth/prompt.py b/src/lib/llm/prompt/get_oauth/prompt.py new file mode 100644 index 0000000..42685fd --- /dev/null +++ b/src/lib/llm/prompt/get_oauth/prompt.py @@ -0,0 +1,61 @@ +prompt = """ +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. +"""