ABOUT ME

-

Total
-
  • Rust: 구글 Bard 바드 CLI 앱 만들기
    컴퓨터/Rust 2023. 4. 28. 17:21
    728x90
    반응형
     

    Bard

    Bard is your creative and helpful collaborator to supercharge your imagination, boost productivity, and bring ideas to life.

    bard.google.com

    소개

    구글의 ChatGPT, 바드가 한국에서 사용할 수 있게 된 지 꽤 되었다.

    성능은... 뭐라 못하겠고 아직 개발 초창기니까 믿어본다.

    꽤 괜찮은 기능은 Bing처럼 Google 인덱싱 된 문서들을 긁어올 수 있고 출처도 남겨준다. (근데 물어봐도 이해를 잘못함)

     

    아무튼 이 글에서는 Bard를 CLI 앱으로 만드는 과정을 적었다. Rust 언어 async로 작성하였다.

     

    How

    우선 어떻게 답변이 생성되는지 확인하기 위해 네트워크에서 다음과 같이 추출했다.

    수많은 쿠키가 있었는데 삭제해 가며 제일 필요한 것은 (only) __Secure-1PSID였다.

    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
    $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
    $session.Cookies.Add((New-Object System.Net.Cookie("SEARCH_SAMESITE", "", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("SID", "Vwgt-JQ.", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("__Secure-1PSID", ".", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("__Secure-3PSID", "Vwgvyl7SkV-.", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("HSID", "", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("SSID", "", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("APISID", "SnUXzWSOT", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("SAPISID", "/", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("__Secure-1PAPISID", "", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("__Secure-3PAPISID", "", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("NID", "511=-6---6OlFP4pLUOCerQ5l2Zr07vRFkSWt1D6oX5AdOp", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("AEC", "AUEFqZeCNsd5YYodUNQBSp4q5ic7J-", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("1P_JAR", "2023-4-27-13", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("SIDCC", "-", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("__Secure-1PSIDCC", "", "/", ".google.com")))
    $session.Cookies.Add((New-Object System.Net.Cookie("__Secure-3PSIDCC", "-", "/", ".google.com")))
    Invoke-WebRequest -UseBasicParsing -Uri "https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?bl=boq_assistant-bard-web-server_20230425.12_p0&_reqid=281956&rt=c" `
    -Method "POST" `
    -WebSession $session `
    -Headers @{
    "authority"="bard.google.com"
      "method"="POST"
      "path"="/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?bl=boq_assistant-bard-web-server_20230425.12_p0&_reqid=281956&rt=c"
      "scheme"="https"
      "accept"="*/*"
      "accept-encoding"="gzip, deflate, br"
      "accept-language"="ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6"
      "dnt"="1"
      "origin"="https://bard.google.com"
      "referer"="https://bard.google.com/"
      "sec-ch-ua"="`"Chromium`";v=`"112`", `"Google Chrome`";v=`"112`", `"Not:A-Brand`";v=`"99`""
      "sec-ch-ua-arch"="`"x86`""
      "sec-ch-ua-bitness"="`"64`""
      "sec-ch-ua-full-version"="`"112.0.5615.138`""
      "sec-ch-ua-full-version-list"="`"Chromium`";v=`"112.0.5615.138`", `"Google Chrome`";v=`"112.0.5615.138`", `"Not:A-Brand`";v=`"99.0.0.0`""
      "sec-ch-ua-mobile"="?0"
      "sec-ch-ua-model"="`"`""
      "sec-ch-ua-platform"="`"Windows`""
      "sec-ch-ua-platform-version"="`"15.0.0`""
      "sec-ch-ua-wow64"="?0"
      "sec-fetch-dest"="empty"
      "sec-fetch-mode"="cors"
      "sec-fetch-site"="same-origin"
      "x-same-domain"="1"
    } `
    -ContentType "application/x-www-form-urlencoded;charset=UTF-8" `
    -Body "f.req=%5Bnull%2C%22%5B%5B%5C%22hey%5C%22%5D%2Cnull%2C%5B%5C%22%5C%22%2C%5C%22%5C%22%2C%5C%22%5C%22%5D%5D%22%5D&at=ABi_lZhwpPHqJT6OW6Y3hnuFGepQ%3A1682603152763&";

     

    그럼 이것을 Rust reqwest를 이용해 바꾸면 된다. 헤더는 다음과 같이 만들 수 있다.

    use reqwest::header::{HeaderMap, HeaderValue, COOKIE, USER_AGENT};
    
    // Existing headers
    let mut headers = HeaderMap::new();
    headers.insert("Host", "bard.google.com".parse()?);
    headers.insert("X-Same-Domain", "1".parse()?);
    headers.insert(
        "User-Agent",
        "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
            .parse()?,
    );
    headers.insert(
        "Content-Type",
        "application/x-www-form-urlencoded;charset=UTF-8".parse()?,
    );
    headers.insert("Origin", "https://bard.google.com".parse()?);
    headers.insert("Referer", "https://bard.google.com/".parse()?);
    headers.append(
        COOKIE,
        HeaderValue::from_str(&
            "__Secure-1PSID=-.; domain=.google.com; Secure; HttpOnly; priority=high",
        )
        .expect("Failed to create header value from session_id"),
    );
    
    headers.insert("accept-encoding", "gzip, deflate, br".parse()?);

     

    하지만 스트림 POST에 필요한 값이 SNlM0e가 있는데 이것은 홈페이지에만 있어서

    홈페이지 GET을 하고 스트림에 POST에 해야 했다. (Body 데이터에 at)

    regex로 받는 단순 작업이므로 넘어가면

    let client = reqwest::Client::builder()
        .default_headers(headers)
        .build()?;
    
    // 1. GET request to https://bard.google.com/
    let resp = client.get("https://bard.google.com/").send().await?;
    let body = resp.text().await?;
            
    // 2. Extract SNlM0e value using regex
    let re = Regex::new(r#"SNlM0e":"(.*?)""#).unwrap();
    let snlm0e = re
        .captures(&body)
        .and_then(|caps| caps.get(1).map(|m| m.as_str()))
        .expect("SNlM0e not found");

     

    이제 POST 파라미터와 form 데이터를 만들어야 하는데 JSON 구조가 이상해서 많이 애쓴 것 같다.

    도대체 어떠한 방식으로 만들었는지 감이 잡히지 않는 실제 Bard 응답 구조이다.

    Vector 안에 Vector로 이루어진 JSON이다. 따라서 첫 줄과 숫자 line들은 버리고

    serde를 이용해 deserialize 하면 아래와 같은데 index에서 좀 헤매었다.

    // Deserialize the JSON string
    let text = post_resp.text().await?;
    
    let lines: Vec<&str> = text.split('\n').collect();
    let json_str = lines[3];
    
    let data: Result<Vec<Vec<Value>>, serde_json::Error> = serde_json::from_str(json_str);
    let chat_data = data
        .as_ref()
        .ok()
        .and_then(|inner_data| inner_data.get(0).and_then(|item| item.get(2)));

    정상 응답

    )]}'
    
    491
    [["wrb.fr",null,"[[\"I am an LLM trained to respond in a subset of languages at this time, so I can't assist you with that. Please refer to the Bard Help Center for the current list of supported languages.\"],[\"c_7bbc5913790342ad\",\"r_7bbc5913790348c9\"],null,null,[[\"rc_7bbc591379034742\",[\"I am an LLM trained to respond in a subset of languages at this time, so I can't assist you with that. Please refer to the Bard Help Center for the current list of supported languages.\"]]]]"]]
    60
    [["di",5479],["af.httprm",5479,"-3336000718962686402",30]]
    25
    [["e",4,null,null,589]]

    실패하면 이런 식으로 400이 보인다.

    ")]}'\n\n101\n[[\"er\",null,null,null,null,400,null,null,null,3],[\"di\",9],[\"af.httprm\",8,\"8966515040052800325\",38]]\n25\n[[\"e\",4,null,null,137]]\n"

     

    저 정상 응답에서 무엇이 의미가 있을까?

    LLM의 재미는 내가 한 대화를 기억하는 것인데 대화를 이어갈 때 몇 숫자가 변하는 것을 알 수 있었다.

    따라서 POST request에는 처음에는 빈 string id이지만 계속 업데이트하는 구조이다.

    url 인코딩을 안 했다가 왜 정상적으로 작동 안 하는 건지 시간을 많이 소모했다.

    // 3. Send POST request
    let message_struct = json!([
        [message],
        (),
        [self.conversation_id, self.response_id, self.choice_id],
    ]);
    let form_data = json!([(), message_struct.to_string()]).to_string();
    
    let body_data = format!(
        "f.req={}&at={}&",
        urlencoding::encode(&form_data),
        urlencoding::encode(&self.snlm0e)
    );
    
    let encoded: String = form_urlencoded::Serializer::new("https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?".to_string())
        .append_pair("bl", "boq_assistant-bard-web-server_20230426.11_p0")
        .append_pair("_reqid", &self.reqid.to_string())
        .append_pair("rt", "c")
        .finish();

     

    이 모든 걸 이제 CLI로 __Secure-1PSID 만 받게 하면 clap을 이용하면 된다.

    use clap::Parser;
    
    /// Google Bard CLI
    #[derive(Parser, Debug)]
    #[command(author = "Seok", version, about = "Google Bard CLI in Rust", long_about = None)]
    struct Args {
        /// Session ID
        #[arg(short, long, help = "About 71 length long, including '.' in the end.")]
        session: String,
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        let args = Args::parse();
    
        let session_id = args.session;
        let mut chatbot = Chatbot::new(&session_id).await?;
        ...
    }

     

    콘솔 창에서 조금 멋지게

    실제 결과물

    위와 같은 형식으로 나오게 (응답 불러올 때는 Bard: thinking...)

    \r (캐리지 리턴)을 나타내는 escape 시퀀스를 이용해서 현재 줄의 시작 위치로 커서를 이동시키는데 유용하다.

     

    결과물은 아래 소스 코드 참조

     

    Alfex4936/Bard-rs: Google Bard CLI with Rust

    Google Bard CLI with Rust.

    github.com

     

    728x90

    댓글