diff --git a/README.md b/README.md
index fd2a5c0..efdc8b4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Sydney.py
-[![Latest Release](https://img.shields.io/github/v/release/vsakkas/sydney.py.svg)](https://github.com/vsakkas/sydney.py/releases/tag/v0.20.6)
+[![Latest Release](https://img.shields.io/github/v/release/vsakkas/sydney.py.svg)](https://github.com/vsakkas/sydney.py/releases/tag/v0.21.0)
[![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/vsakkas/sydney.py/blob/master/LICENSE)
@@ -17,6 +17,7 @@ Python Client for Copilot (formerly named Bing Chat), also known as Sydney.
- Stream response tokens for real-time communication.
- Retrieve citations and suggested user responses.
- Enhance your prompts with images for an enriched experience.
+- Customize your experience using any of the supported personas.
- Use asyncio for efficient and non-blocking I/O operations.
## Requirements
@@ -203,6 +204,25 @@ Searching the web is enabled by default.
> [!NOTE]
> Web search cannot be disabled when the response is streamed.
+
+### Personas
+
+It is possible to use specialized versions of Copilot, suitable for specific tasks or conversations:
+
+```python
+async with SydneyClient(persona="travel") as sydney:
+ response = await sydney.ask("Tourist attractions in Sydney")
+ print(response)
+```
+
+The available options for the `persona` parameter are:
+- `copilot`
+- `travel`
+- `cooking`
+- `fitness`
+
+By default, Sydney will use the `copilot` persona.
+
### Compose
You can ask Copilot to compose different types of content, such emails, articles, ideas and more:
diff --git a/pyproject.toml b/pyproject.toml
index 5eab796..d618ce4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sydney-py"
-version = "0.20.6"
+version = "0.21.0"
description = "Python Client for Copilot (formerly named Bing Chat), also known as Sydney."
authors = ["vsakkas "]
license = "MIT"
diff --git a/sydney/enums.py b/sydney/enums.py
index 2ab92db..159fbff 100644
--- a/sydney/enums.py
+++ b/sydney/enums.py
@@ -59,6 +59,16 @@ class NoSearchOptions(Enum):
NOSEARCHALL = "nosearchall"
+class PersonaOptions(Enum):
+ """
+ Options that are used with the non default GPT personas.
+ """
+
+ TRAVEL = "ai_persona_vacation_planner_with_examples"
+ COOKING = "ai_persona_cooking_assistant_w1shot"
+ FITNESS = "ai_persona_fitness_trainer_w1shot"
+
+
class DefaultComposeOptions(Enum):
"""
Options that are used in all compose API requests to Copilot.
@@ -160,6 +170,21 @@ class MessageType(Enum):
SEARCH_QUERY = "SearchQuery"
+class GPTPersonaID(Enum):
+ """
+ Allowed IDs for different GPT personas. Supported options are:
+ - `copilot` for using the default Copilot persona
+ - `travel` for using the vacation planner persona
+ - `cooking` for using the cooking assistant persona
+ - `fitness` for using the fitness trainer persona
+ """
+
+ COPILOT = "copilot"
+ TRAVEL = "travel"
+ COOKING = "cooking"
+ FITNESS = "fitness"
+
+
class ResultValue(Enum):
"""
Copilot result values on raw responses. Supported options are:
diff --git a/sydney/sydney.py b/sydney/sydney.py
index 6a94092..b313df8 100644
--- a/sydney/sydney.py
+++ b/sydney/sydney.py
@@ -33,8 +33,10 @@
CustomComposeTone,
DefaultComposeOptions,
DefaultOptions,
+ GPTPersonaID,
MessageType,
NoSearchOptions,
+ PersonaOptions,
ResultValue,
)
from sydney.exceptions import (
@@ -55,6 +57,7 @@ class SydneyClient:
def __init__(
self,
style: str = "balanced",
+ persona: str = "copilot",
bing_cookies: str | None = None,
use_proxy: bool = False,
) -> None:
@@ -66,6 +69,9 @@ def __init__(
style : str
The conversation style that Copilot will adopt. Must be one of the options listed
in the `ConversationStyle` enum. Default is "balanced".
+ persona : str
+ The GPT persona that Copilot will adopt. Must be one of the options listed in the
+ `GPTPersonaID` enum. Default is "copilot".
bing_cookies: str | None
The cookies from Bing required to connect and use Copilot. If not provided,
the `BING_COOKIES` environment variable is loaded instead. Default is None.
@@ -82,6 +88,7 @@ def __init__(
self.conversation_style_option_sets: ConversationStyleOptionSets = getattr(
ConversationStyleOptionSets, style.upper()
)
+ self.persona: GPTPersonaID = getattr(GPTPersonaID, persona.upper())
self.conversation_signature: str | None = None
self.encrypted_conversation_signature: str | None = None
self.conversation_id: str | None = None
@@ -142,6 +149,10 @@ def _build_ask_arguments(
if not search:
options_sets.extend(option.value for option in NoSearchOptions)
+ # Build option sets based on whether a non default GPT persona is used or not.
+ if self.persona != GPTPersonaID.COPILOT:
+ options_sets.append(PersonaOptions[self.persona.value.upper()].value)
+
image_url, original_image_url = None, None
if attachment_info:
image_url = BING_BLOB_URL + attachment_info["blobId"]
@@ -160,7 +171,7 @@ def _build_ask_arguments(
"conversationHistoryOptionsSets": [
option.value for option in ConversationHistoryOptionsSets
],
- "gptId": "copilot",
+ "gptId": self.persona.value,
"isStartOfSession": self.invocation_id == 0,
"message": {
"author": "user",
@@ -175,6 +186,9 @@ def _build_ask_arguments(
"id": self.client_id,
},
"tone": str(self.conversation_style.value),
+ "extraExtensionParameters": {
+ "gpt-creator-persona": {"personaId": self.persona.value}
+ },
"spokenTextMode": "None",
"conversationId": self.conversation_id,
}
diff --git a/tests/test_ask.py b/tests/test_ask.py
index c5629d7..2875215 100644
--- a/tests/test_ask.py
+++ b/tests/test_ask.py
@@ -263,7 +263,65 @@ async def test_ask_logic_precise() -> bool:
score = 0
for expected_response in expected_responses:
score = fuzz.token_sort_ratio(response, expected_response)
- if score >= 80:
+ if score >= 75:
+ return True
+
+ assert False, f"Unexpected response: {response}, match score: {score}"
+
+
+@pytest.mark.asyncio
+async def test_ask_travel_persona() -> bool:
+ expected_responses = [
+ "Hello! This is Vacation Planner. How can I assist you with your vacation plans today? 😊",
+ "Hello! This is Vacation Planner. How can I assist you with your vacation plans? 😊",
+ ]
+
+ async with SydneyClient(persona="travel") as sydney:
+ response = await sydney.ask("Hello, Copilot!")
+
+ score = 0
+ for expected_response in expected_responses:
+ score = fuzz.token_sort_ratio(response, expected_response)
+ if score >= 75:
+ return True
+
+ assert False, f"Unexpected response: {response}, match score: {score}"
+
+
+@pytest.mark.asyncio
+async def test_ask_travel_cooking() -> bool:
+ expected_responses = [
+ "Hello! This is Cooking Assistant. How can I assist you today? 😊",
+ "Hello! This is Cooking Assistant. How can I assist you in the kitchen today? 😊",
+ ]
+
+ async with SydneyClient(persona="cooking") as sydney:
+ response = await sydney.ask("Hello, Copilot!")
+
+ score = 0
+ for expected_response in expected_responses:
+ score = fuzz.token_sort_ratio(response, expected_response)
+ if score >= 75:
+ return True
+
+ assert False, f"Unexpected response: {response}, match score: {score}"
+
+
+@pytest.mark.asyncio
+async def test_ask_travel_fitness() -> bool:
+ expected_responses = [
+ "Hello! How can I assist you with your fitness journey today? 😊",
+ "Hello! This is Fitness Trainer. How can I assist you today? 😊",
+ "Hello! This is Fitness Trainer. How can I assist you with your fitness journey today? 💪",
+ ]
+
+ async with SydneyClient(persona="fitness") as sydney:
+ response = await sydney.ask("Hello, Copilot!")
+
+ score = 0
+ for expected_response in expected_responses:
+ score = fuzz.token_sort_ratio(response, expected_response)
+ if score >= 75:
return True
assert False, f"Unexpected response: {response}, match score: {score}"