Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Commit

Permalink
Add Bard client (#5)
Browse files Browse the repository at this point in the history
- Add Bard client that supports starting a conversation, sending prompts, resetting and closing the conversation
- Add first test
- Add tests pipeline
  • Loading branch information
vsakkas authored Nov 6, 2023
1 parent c375688 commit 301b3d2
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 6 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test

on:
push:
branches:
- master
pull_request:
branches:
- master

env:
SECURE_1PSID: ${{ secrets.SECURE_1PSID }}
SECURE_1PSIDTS: ${{ secrets.SECURE_1PSIDTS }}

jobs:
build-and-test:
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.10", "3.11"]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install

- name: Run type checker
run: poetry run mypy --ignore-missing-imports bard/

- name: Run tests
run: poetry run pytest --cov
113 changes: 110 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Bard.py
# <img src="https://raw.githubusercontent.com/vsakkas/bard.py/master/images/logo.svg?token=GHSAT0AAAAAAB7MEK465TODCKRPHN3YQY54ZKGUN4Q" width="24px" /> Bard.py

[![Latest Release](https://img.shields.io/github/v/release/vsakkas/bard.py.svg)](https://github.com/vsakkas/bard.py/releases/tag/v0.1.0)
[![Latest Release](https://img.shields.io/github/v/release/vsakkas/bard.py.svg)](https://github.com/vsakkas/bard.py/releases/tag/v0.2.0)
[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/vsakkas/bard.py/blob/master/LICENSE)

Python client for Bard.
Python Client for Bard, a Chat Based AI tool by Google.

> **Note**
> This is an **unofficial** client.
## Features

- Connect to Bard, Google's AI-powered personal assistant.
- Ask questions and have a conversation.
- Use asyncio for efficient and non-blocking I/O operations.

## Requirements

- Python 3.10 or newer
Expand All @@ -28,6 +34,107 @@ or, if you use [poetry](https://python-poetry.org/):
poetry add bard-py
```

> **Note**
> Make sure you're using the latest version of Bard.py to ensure best compatibility with Bard.
## Usage

### Prerequisites

To use Bard.py you first need to extract the `__Secure-1PSID` and `__Secure-1PSIDTS` cookies from the Bard web page. These cookies are used to authenticate your requests to the Bard API.

To get the cookies, follow these steps on Chrome:
- Go to the [Bard web page](https://bard.google.com/).
- Write a message on the chat dialog that appears.
- Open the developer tools in your browser (usually by pressing `F12` or right-clicking on the chat dialog and selecting `Inspect`).
- Select the `Application` tab and click on the `Cookies` option to view all cookies associated with `https://bard.google.com`.
- Look for the `__Secure-1PSID` and `__Secure-1PSIDTS` cookies and click on them to expand their details.
- Copy the values of the cookies (they should look like a long string of letters and numbers).

Then, set them as environment variables in your shell:

```bash
export SECURE_1PSID=<your-cookie>
export SECURE_1PSIDTS=<your-other-cookie>
```

or, in your Python code:

```python
os.environ["SECURE_1PSID"] = "<your-cookie>"
os.environ["SECURE_1PSIDTS"] = "<your-other-cookie>"
```

### Example

You can use Bard.py to easily create a CLI client for Bard:

```python
import asyncio

from bard import BardClient


async def main() -> None:
async with BardClient() as bard:
while True:
prompt = input("You: ")

if prompt == "!reset":
await bard.reset_conversation()
continue
elif prompt == "!exit":
break

response = await bard.ask(prompt)
print(f"Bard: {response}")


if __name__ == "__main__":
asyncio.run(main())
```

### Bard Client

You can create a Bard Client and initialize a connection with Bard which starts a conversation:

```python
bard = BardClient()

await bard.start_conversation()

# Conversation

await bard.end_conversation()
```

Alternatively, you can use the `async with` statement to keep the code compact:

```python
async with BardClient() as bard:
# Conversation
```

### Reset Conversation

You can reset the conversation in order to make the client forget the previous conversation:

```python
async with BardClient() as bard:
# Conversation
await bard.reset_conversation()
```

### Ask

You can ask Bard questions and get the results:

```python
async with BardClient() as bard:
response = await bard.ask("When was Bard released?")
print(response)
```

## License

This project is licensed under the MIT License - see the [LICENSE](https://github.com/vsakkas/bard.py/blob/master/LICENSE) file for details.
151 changes: 149 additions & 2 deletions bard/bard.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,150 @@
import json
import random
import re
from os import environ

from aiohttp import ClientSession

from bard.constants import BARD_STREAM_GENERATE_URL, BARD_URL, HEADERS
from bard.exceptions import AskException, CreateConversationException
from bard.utils import double_json_stringify


class BardClient:
def __init__(self) -> None:
pass
def __init__(
self, secure_1psid: str | None = None, secure_1psidts: str | None = None
) -> None:
"""
Client for Bard.
"""
self.secure_1psid = secure_1psid if secure_1psid else environ["SECURE_1PSID"]
self.secure_1psidts = (
secure_1psidts if secure_1psidts else environ["SECURE_1PSIDTS"]
)
self.conversation_id: str | None = None
self.response_id: str | None = None
self.choice_id: str | None = None
self.session: ClientSession | None = None

async def __aenter__(self) -> "BardClient":
await self.start_conversation()
return self

async def __aexit__(self, exc_type, exc_value, traceback) -> None:
await self.close_conversation()

async def _get_session(self, force_close: bool = False) -> ClientSession:
# Use cookies to create a conversation.
cookies = {
"__Secure-1PSID": self.secure_1psid,
"__Secure-1PSIDTS": self.secure_1psidts,
}

if self.session and force_close:
await self.session.close()

if not self.session:
self.session = ClientSession(
headers=HEADERS,
cookies=cookies,
)

return self.session

def _build_ask_parameters(self) -> dict:
return {
"bl": "boq_assistant-bard-web-server_20231031.09_p7",
"_reqid": "".join(str(random.randint(0, 9)) for _ in range(7)),
"rt": "c",
}

def _build_ask_arguments(self, prompt: str) -> dict:
request_data = [
[prompt],
None,
[self.conversation_id, self.response_id, self.choice_id],
]

return {
"f.req": double_json_stringify(request_data),
"at": self.snlm0e,
}

async def start_conversation(self) -> None:
"""
Connect to Bard and create a new conversation.
"""
session = await self._get_session(force_close=True)

async with session.get(BARD_URL) as response:
if response.status != 200:
raise CreateConversationException(
f"Failed to create conversation, received status: {response.status}"
)

response_text = await response.text()

snlm0e_dict = re.search(r"\"SNlM0e\":\"(?P<value>.*?)\"", response_text)

if not snlm0e_dict:
raise CreateConversationException(
"Failed to create conversation, SNlM0e value was not found."
)

self.snlm0e = snlm0e_dict.group("value")

async def ask(self, prompt: str) -> str:
"""
Send a prompt to Bard and return the answer.
Parameters
----------
prompt: str
The prompt that needs to be sent to Bard.
Returns
-------
str
The response from Bard.
"""
parameters = self._build_ask_parameters()
arguments = self._build_ask_arguments(prompt)

session = await self._get_session()

async with session.post(
BARD_STREAM_GENERATE_URL, params=parameters, data=arguments
) as response:
if response.status != 200:
raise AskException(
f"Failed to get response, received status: {response.status}"
)

response_text = await response.text()
response_data = json.loads(response_text.splitlines()[3])

message = json.loads(response_data[0][2])

self.conversation_id = message[1][0]
self.response_id = message[1][1]
self.choice_id = message[4][0][0]

return message[4][0][1][0]

async def reset_conversation(self) -> None:
"""
Clear current conversation information and connection and start new ones.
"""
await self.close_conversation()
await self.start_conversation()

async def close_conversation(self) -> None:
"""
Close all connections to Bard. Clear conversation information.
"""
if self.session and not self.session.closed:
await self.session.close()

self.conversation_id = None
self.response_id = None
self.choice_id = None
21 changes: 21 additions & 0 deletions bard/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"

HEADERS = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Origin": "https://bard.google.com",
"Referer": "https://bard.google.com/",
"Sec-Ch-Ua": '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": "Windows",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": USER_AGENT,
"X-Same-Domain": "1",
}

BARD_URL = "https://bard.google.com/chat"
BARD_STREAM_GENERATE_URL = "https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
6 changes: 6 additions & 0 deletions bard/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class CreateConversationException(Exception):
pass


class AskException(Exception):
pass
5 changes: 5 additions & 0 deletions bard/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import json


def double_json_stringify(data) -> str:
return json.dumps([None, json.dumps(data)])
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bard-py"
version = "0.1.0"
version = "0.2.0"
description = "Python Client for Bard."
authors = ["vsakkas <[email protected]>"]
license = "MIT"
Expand Down
9 changes: 9 additions & 0 deletions tests/test_ask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pytest

from bard import BardClient


@pytest.mark.asyncio
async def test_ask() -> None:
async with BardClient() as bard:
_ = await bard.ask("Tell me a joke")

0 comments on commit 301b3d2

Please sign in to comment.