Skip to content
This repository was archived by the owner on Nov 11, 2025. It is now read-only.

Commit 2bbf6b0

Browse files
authored
Add support for personas (#170)
- Add support for `travel`, `cooking` and `fitness` personas - Add testing for the personas - Update documentation with usage of the personas
1 parent 4a275d3 commit 2bbf6b0

File tree

5 files changed

+121
-4
lines changed

5 files changed

+121
-4
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# <img src="https://raw.githubusercontent.com/vsakkas/sydney.py/master/images/logo.svg" width="28px" /> Sydney.py
22

3-
[![Latest Release](https://img.shields.io/github/v/release/vsakkas/sydney.py.svg)](https://github.com/vsakkas/sydney.py/releases/tag/v0.20.6)
3+
[![Latest Release](https://img.shields.io/github/v/release/vsakkas/sydney.py.svg)](https://github.com/vsakkas/sydney.py/releases/tag/v0.21.0)
44
[![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
55
[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/vsakkas/sydney.py/blob/master/LICENSE)
66

@@ -17,6 +17,7 @@ Python Client for Copilot (formerly named Bing Chat), also known as Sydney.
1717
- Stream response tokens for real-time communication.
1818
- Retrieve citations and suggested user responses.
1919
- Enhance your prompts with images for an enriched experience.
20+
- Customize your experience using any of the supported personas.
2021
- Use asyncio for efficient and non-blocking I/O operations.
2122

2223
## Requirements
@@ -203,6 +204,25 @@ Searching the web is enabled by default.
203204
> [!NOTE]
204205
> Web search cannot be disabled when the response is streamed.
205206
207+
208+
### Personas
209+
210+
It is possible to use specialized versions of Copilot, suitable for specific tasks or conversations:
211+
212+
```python
213+
async with SydneyClient(persona="travel") as sydney:
214+
response = await sydney.ask("Tourist attractions in Sydney")
215+
print(response)
216+
```
217+
218+
The available options for the `persona` parameter are:
219+
- `copilot`
220+
- `travel`
221+
- `cooking`
222+
- `fitness`
223+
224+
By default, Sydney will use the `copilot` persona.
225+
206226
### Compose
207227

208228
You can ask Copilot to compose different types of content, such emails, articles, ideas and more:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sydney-py"
3-
version = "0.20.6"
3+
version = "0.21.0"
44
description = "Python Client for Copilot (formerly named Bing Chat), also known as Sydney."
55
authors = ["vsakkas <[email protected]>"]
66
license = "MIT"

sydney/enums.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ class NoSearchOptions(Enum):
5959
NOSEARCHALL = "nosearchall"
6060

6161

62+
class PersonaOptions(Enum):
63+
"""
64+
Options that are used with the non default GPT personas.
65+
"""
66+
67+
TRAVEL = "ai_persona_vacation_planner_with_examples"
68+
COOKING = "ai_persona_cooking_assistant_w1shot"
69+
FITNESS = "ai_persona_fitness_trainer_w1shot"
70+
71+
6272
class DefaultComposeOptions(Enum):
6373
"""
6474
Options that are used in all compose API requests to Copilot.
@@ -160,6 +170,21 @@ class MessageType(Enum):
160170
SEARCH_QUERY = "SearchQuery"
161171

162172

173+
class GPTPersonaID(Enum):
174+
"""
175+
Allowed IDs for different GPT personas. Supported options are:
176+
- `copilot` for using the default Copilot persona
177+
- `travel` for using the vacation planner persona
178+
- `cooking` for using the cooking assistant persona
179+
- `fitness` for using the fitness trainer persona
180+
"""
181+
182+
COPILOT = "copilot"
183+
TRAVEL = "travel"
184+
COOKING = "cooking"
185+
FITNESS = "fitness"
186+
187+
163188
class ResultValue(Enum):
164189
"""
165190
Copilot result values on raw responses. Supported options are:

sydney/sydney.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
CustomComposeTone,
3434
DefaultComposeOptions,
3535
DefaultOptions,
36+
GPTPersonaID,
3637
MessageType,
3738
NoSearchOptions,
39+
PersonaOptions,
3840
ResultValue,
3941
)
4042
from sydney.exceptions import (
@@ -55,6 +57,7 @@ class SydneyClient:
5557
def __init__(
5658
self,
5759
style: str = "balanced",
60+
persona: str = "copilot",
5861
bing_cookies: str | None = None,
5962
use_proxy: bool = False,
6063
) -> None:
@@ -66,6 +69,9 @@ def __init__(
6669
style : str
6770
The conversation style that Copilot will adopt. Must be one of the options listed
6871
in the `ConversationStyle` enum. Default is "balanced".
72+
persona : str
73+
The GPT persona that Copilot will adopt. Must be one of the options listed in the
74+
`GPTPersonaID` enum. Default is "copilot".
6975
bing_cookies: str | None
7076
The cookies from Bing required to connect and use Copilot. If not provided,
7177
the `BING_COOKIES` environment variable is loaded instead. Default is None.
@@ -82,6 +88,7 @@ def __init__(
8288
self.conversation_style_option_sets: ConversationStyleOptionSets = getattr(
8389
ConversationStyleOptionSets, style.upper()
8490
)
91+
self.persona: GPTPersonaID = getattr(GPTPersonaID, persona.upper())
8592
self.conversation_signature: str | None = None
8693
self.encrypted_conversation_signature: str | None = None
8794
self.conversation_id: str | None = None
@@ -142,6 +149,10 @@ def _build_ask_arguments(
142149
if not search:
143150
options_sets.extend(option.value for option in NoSearchOptions)
144151

152+
# Build option sets based on whether a non default GPT persona is used or not.
153+
if self.persona != GPTPersonaID.COPILOT:
154+
options_sets.append(PersonaOptions[self.persona.value.upper()].value)
155+
145156
image_url, original_image_url = None, None
146157
if attachment_info:
147158
image_url = BING_BLOB_URL + attachment_info["blobId"]
@@ -160,7 +171,7 @@ def _build_ask_arguments(
160171
"conversationHistoryOptionsSets": [
161172
option.value for option in ConversationHistoryOptionsSets
162173
],
163-
"gptId": "copilot",
174+
"gptId": self.persona.value,
164175
"isStartOfSession": self.invocation_id == 0,
165176
"message": {
166177
"author": "user",
@@ -175,6 +186,9 @@ def _build_ask_arguments(
175186
"id": self.client_id,
176187
},
177188
"tone": str(self.conversation_style.value),
189+
"extraExtensionParameters": {
190+
"gpt-creator-persona": {"personaId": self.persona.value}
191+
},
178192
"spokenTextMode": "None",
179193
"conversationId": self.conversation_id,
180194
}

tests/test_ask.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,65 @@ async def test_ask_logic_precise() -> bool:
263263
score = 0
264264
for expected_response in expected_responses:
265265
score = fuzz.token_sort_ratio(response, expected_response)
266-
if score >= 80:
266+
if score >= 75:
267+
return True
268+
269+
assert False, f"Unexpected response: {response}, match score: {score}"
270+
271+
272+
@pytest.mark.asyncio
273+
async def test_ask_travel_persona() -> bool:
274+
expected_responses = [
275+
"Hello! This is Vacation Planner. How can I assist you with your vacation plans today? 😊",
276+
"Hello! This is Vacation Planner. How can I assist you with your vacation plans? 😊",
277+
]
278+
279+
async with SydneyClient(persona="travel") as sydney:
280+
response = await sydney.ask("Hello, Copilot!")
281+
282+
score = 0
283+
for expected_response in expected_responses:
284+
score = fuzz.token_sort_ratio(response, expected_response)
285+
if score >= 75:
286+
return True
287+
288+
assert False, f"Unexpected response: {response}, match score: {score}"
289+
290+
291+
@pytest.mark.asyncio
292+
async def test_ask_travel_cooking() -> bool:
293+
expected_responses = [
294+
"Hello! This is Cooking Assistant. How can I assist you today? 😊",
295+
"Hello! This is Cooking Assistant. How can I assist you in the kitchen today? 😊",
296+
]
297+
298+
async with SydneyClient(persona="cooking") as sydney:
299+
response = await sydney.ask("Hello, Copilot!")
300+
301+
score = 0
302+
for expected_response in expected_responses:
303+
score = fuzz.token_sort_ratio(response, expected_response)
304+
if score >= 75:
305+
return True
306+
307+
assert False, f"Unexpected response: {response}, match score: {score}"
308+
309+
310+
@pytest.mark.asyncio
311+
async def test_ask_travel_fitness() -> bool:
312+
expected_responses = [
313+
"Hello! How can I assist you with your fitness journey today? 😊",
314+
"Hello! This is Fitness Trainer. How can I assist you today? 😊",
315+
"Hello! This is Fitness Trainer. How can I assist you with your fitness journey today? 💪",
316+
]
317+
318+
async with SydneyClient(persona="fitness") as sydney:
319+
response = await sydney.ask("Hello, Copilot!")
320+
321+
score = 0
322+
for expected_response in expected_responses:
323+
score = fuzz.token_sort_ratio(response, expected_response)
324+
if score >= 75:
267325
return True
268326

269327
assert False, f"Unexpected response: {response}, match score: {score}"

0 commit comments

Comments
 (0)