diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..071006c
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/README.md b/README.md
index 5c6f9f1..54de495 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,20 @@
-# Bard.py
+# 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
@@ -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=
+export SECURE_1PSIDTS=
+```
+
+or, in your Python code:
+
+```python
+os.environ["SECURE_1PSID"] = ""
+os.environ["SECURE_1PSIDTS"] = ""
+```
+
+### 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.
diff --git a/bard/bard.py b/bard/bard.py
index 6b307dc..a4510f4 100644
--- a/bard/bard.py
+++ b/bard/bard.py
@@ -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.*?)\"", 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
diff --git a/bard/constants.py b/bard/constants.py
new file mode 100644
index 0000000..121b14b
--- /dev/null
+++ b/bard/constants.py
@@ -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"
diff --git a/bard/exceptions.py b/bard/exceptions.py
new file mode 100644
index 0000000..6711629
--- /dev/null
+++ b/bard/exceptions.py
@@ -0,0 +1,6 @@
+class CreateConversationException(Exception):
+ pass
+
+
+class AskException(Exception):
+ pass
diff --git a/bard/utils.py b/bard/utils.py
new file mode 100644
index 0000000..e9a9835
--- /dev/null
+++ b/bard/utils.py
@@ -0,0 +1,5 @@
+import json
+
+
+def double_json_stringify(data) -> str:
+ return json.dumps([None, json.dumps(data)])
diff --git a/pyproject.toml b/pyproject.toml
index f61c517..cb3a429 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bard-py"
-version = "0.1.0"
+version = "0.2.0"
description = "Python Client for Bard."
authors = ["vsakkas "]
license = "MIT"
diff --git a/tests/test_ask.py b/tests/test_ask.py
new file mode 100644
index 0000000..9f7e090
--- /dev/null
+++ b/tests/test_ask.py
@@ -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")