Skip to content

Commit

Permalink
Add documentation on TextsProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
Saluev committed Dec 17, 2023
1 parent 997e3b4 commit 8f4542d
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 107 deletions.
55 changes: 55 additions & 0 deletions docs/usage/customizing_texts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Customizing texts

Suppgram encapsulates all text generation in [TextsProvider][suppgram.texts.TextsProvider] class.

::: suppgram.texts.TextsProvider
handler: python
options:
show_root_heading: true
show_source: false
heading_level: 2

It has a lot of members, see [source code](https://github.com/Saluev/suppgram/blob/master/suppgram/texts/interface.py)
for details.

Suppgram also provides out-of-the-box text pack for English language.

::: suppgram.texts.en.EnglishTextsProvider
handler: python
options:
show_root_heading: true
show_source: false
heading_level: 2

If you want to customize your texts, you can either implement your own `TextsProvider` or tweak an
existing one. For example, let's say we want to hide customer contacts from agents for some privacy reasons.
Our code would look like the following:

```python
# ./mytexts.py

from suppgram.texts.en import EnglishTextsProvider


class MyTextsProvider(EnglishTextsProvider):
customer_profile_contacts = "📒 Contacts: (hidden)"
```

Then we can customize texts provider in all-in-one CLI:
```shell
$ python -m suppgram.cli.all_in_one \
--sqlalchemy-uri sqlite+aiosqlite:///test.db \
--telegram-owner-id <your Telegram user ID> \
--texts mytexts.MyTextsProvider
```
or via builder:
```python
from suppgram.builder import Builder

from mytexts import MyTextsProvider


builder = Builder()
...
builder = builder.with_texts(MyTextsProvider())
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ nav:
- Quickstart: usage/quickstart.md
- 'All-in-one CLI': usage/all_in_one_cli.md
- Builder: usage/builder.md
- 'Customizing texts': usage/customizing_texts.md
- Development:
- 'Contribution guide': development/contribution_guide.md
- 'Architecture overview': development/architecture_overview.md
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "suppgram"
version = "0.0.3"
version = "0.0.4"
authors = [
{ name="Tigran Saluev", email="tigran@saluev.com" },
]
Expand Down
2 changes: 1 addition & 1 deletion suppgram/frontends/telegram/manager_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ async def _update_new_conversation_notification(
if "Message is not modified" not in str(exc):
raise

text = self._texts.compose_telegram_new_conversation_notification(conversation)
text = self._texts.compose_telegram_conversation_notification(conversation)
try:
await self._telegram_bot.edit_message_text(
text.text,
Expand Down
108 changes: 7 additions & 101 deletions suppgram/texts/en.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import html
import logging
from typing import Optional, Collection

from suppgram.entities import (
Conversation,
Message,
MessageKind,
Customer,
ConversationTag,
Agent,
)
from suppgram.helpers import escape_markdown
from suppgram.texts.interface import TextsProvider, Text, Format

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -59,12 +54,6 @@ def compose_customer_conversation_resolved_message(self, rating: int) -> str:
telegram_tag_successfully_created_message = "✅ New tag has been successfully created."
telegram_tag_already_exists_message = "⚠️ Tag with this name already exists!"

def compose_add_tag_button_text(self, tag: ConversationTag) -> str:
return f"☐ {tag.name}"

def compose_remove_tag_button_text(self, tag: ConversationTag) -> str:
return f"☑ {tag.name}"

telegram_agent_start_message = "👷 Welcome to the support agent bot!"
telegram_agent_permission_denied_message = (
"🚫 You don't have permission to access support agent functionality."
Expand All @@ -83,65 +72,15 @@ def compose_remove_tag_button_text(self, tag: ConversationTag) -> str:
)
telegram_new_conversation_notification_placeholder = "❗️ New conversation!"

# TODO move logic to base class, keep only string templates here
def compose_customer_profile(
self, customer: Customer, allowed_formats: Collection[Format] = (Format.PLAIN,)
) -> Text:
format_ = next(iter(allowed_formats))
if Format.TELEGRAM_HTML in allowed_formats:
format_ = Format.TELEGRAM_HTML
elif Format.TELEGRAM_MARKDOWN in allowed_formats:
format_ = Format.TELEGRAM_MARKDOWN
customer_profile_header = "👤 Customer: {customer}"
customer_profile_anonymous = "anonymous"
customer_profile_contacts = "📒 Contacts: {contacts}"

full_name = (
f"{customer.telegram_first_name or ''} {customer.telegram_last_name or ''}".strip()
or "anonymous"
)
lines = [f"👤 Customer: {full_name}"]
contacts = []
if customer.telegram_user_id:
contacts.append(
self._format_telegram_mention(
telegram_user_id=customer.telegram_user_id,
telegram_first_name="Telegram",
telegram_last_name=None,
telegram_username=customer.telegram_username,
format_=format_,
)
)
lines.append("Contacts: " + ", ".join(contacts))
return Text(text="\n".join(lines), format=format_)

def compose_telegram_new_conversation_notification(self, conversation: Conversation) -> Text:
profile = self.compose_customer_profile(
conversation.customer,
allowed_formats=Format.get_formats_supported_by_telegram(),
)
def compose_conversation_notification_header(self, conversation: Conversation) -> str:
emoji = self.CONVERSATION_STATE_TO_EMOJI.get(conversation.state, "")
lines = [
f"{emoji} Conversation in status #{conversation.state.upper()}",
"",
profile.text,
"",
]
lines.extend(self.format_history_message(message) for message in conversation.messages)
if agent := conversation.assigned_agent:
if agent.telegram_user_id:
agent_ref = self._format_telegram_mention(
telegram_user_id=agent.telegram_user_id,
telegram_first_name=agent.telegram_first_name,
telegram_last_name=None, # less formal
telegram_username=agent.telegram_username,
format_=profile.format,
)
else:
logger.warning(f"Can't mention {agent} — unsupported agent frontend")
agent_ref = f"#_{agent.id}"
lines.extend(("", f"Assigned to {agent_ref}"))
if conversation.tags:
tags = [self._format_telegram_tag(tag) for tag in conversation.tags]
lines.extend(("", " ".join(tags)))
return Text(text="\n".join(lines), format=profile.format)
return f"{emoji} Conversation in status #{conversation.state.upper()}"

conversation_notification_assigned_to = "Assigned to {agent}"

def compose_nudge_to_start_bot_notification(
self, agent: Agent, telegram_bot_username: str
Expand All @@ -163,39 +102,6 @@ def compose_nudge_to_start_bot_notification(

telegram_assign_to_me_button_text = "Assign to me"

def _format_telegram_mention(
self,
telegram_user_id: int,
telegram_first_name: Optional[str],
telegram_last_name: Optional[str],
telegram_username: Optional[str],
format_: Format,
) -> str:
full_name: str = (
f"{telegram_first_name or ''} {telegram_last_name or ''}".strip()
or telegram_username
or str(telegram_user_id)
)

if format_ == Format.PLAIN:
if not telegram_username:
logger.warning(
f"Can't mention Telegram user {telegram_user_id} without username in plain format"
)
return f"@{telegram_username}" if telegram_username else full_name

url = f"tg://user?id={telegram_user_id}"

if format_ == Format.TELEGRAM_MARKDOWN:
escaped_name = escape_markdown(full_name)
return f"[{escaped_name}]({url})"

if format_ == Format.TELEGRAM_HTML:
escaped_name = html.escape(full_name)
return f'<a href="{url}">{escaped_name}</a>'

raise ValueError(f"text format {format_.value!r} is not supported")

message_history_title = "🗂️ Message history\n"

def format_history_message(self, message: Message) -> str:
Expand Down
105 changes: 101 additions & 4 deletions suppgram/texts/interface.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import html
import logging
import re
from dataclasses import dataclass
Expand All @@ -13,6 +14,7 @@
Agent,
Message,
)
from suppgram.helpers import escape_markdown

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -49,6 +51,8 @@ def parse_mode(self) -> Optional[str]:


class TextsProvider:
"""Provides static texts and functions to compose dynamic texts where necessary."""

telegram_customer_start_message: str
telegram_customer_conversation_resolved_message_placeholder: str
telegram_customer_conversation_resolved_message: str
Expand All @@ -71,10 +75,10 @@ def compose_customer_conversation_resolved_message(self, rating: int) -> str:
telegram_tag_successfully_created_message: str

def compose_add_tag_button_text(self, tag: ConversationTag) -> str:
raise NotImplementedError
return f"☐ {tag.name}"

def compose_remove_tag_button_text(self, tag: ConversationTag) -> str:
raise NotImplementedError
return f"☑ {tag.name}"

telegram_agent_start_message: str
telegram_agent_permission_denied_message: str
Expand All @@ -85,27 +89,120 @@ def compose_remove_tag_button_text(self, tag: ConversationTag) -> str:

telegram_new_conversation_notification_placeholder: str

customer_profile_header: str
customer_profile_anonymous: str
customer_profile_contacts: str

def compose_customer_profile(
self, customer: Customer, allowed_formats: Collection[Format] = (Format.PLAIN,)
) -> Text:
raise NotImplementedError
format_ = next(iter(allowed_formats))
if Format.TELEGRAM_HTML in allowed_formats:
format_ = Format.TELEGRAM_HTML
elif Format.TELEGRAM_MARKDOWN in allowed_formats:
format_ = Format.TELEGRAM_MARKDOWN

full_name = (
f"{customer.telegram_first_name or ''} {customer.telegram_last_name or ''}".strip()
or self.customer_profile_anonymous
)
lines = [self.customer_profile_header.format(customer=full_name)]
contacts = []
if customer.telegram_user_id:
contacts.append(
self._format_telegram_mention(
telegram_user_id=customer.telegram_user_id,
telegram_first_name="Telegram",
telegram_last_name=None,
telegram_username=customer.telegram_username,
format_=format_,
)
)
if contacts:
lines.append(self.customer_profile_contacts.format(contacts=", ".join(contacts)))
return Text(text="\n".join(lines), format=format_)

CONVERSATION_STATE_TO_EMOJI = {
ConversationState.NEW: "❗️",
ConversationState.ASSIGNED: "⏳",
ConversationState.RESOLVED: "✅",
}

def compose_telegram_new_conversation_notification(self, conversation: Conversation) -> Text:
def compose_conversation_notification_header(self, conversation: Conversation) -> str:
raise NotImplementedError

conversation_notification_assigned_to: str

def compose_telegram_conversation_notification(self, conversation: Conversation) -> Text:
profile = self.compose_customer_profile(
conversation.customer,
allowed_formats=Format.get_formats_supported_by_telegram(),
)
lines = [
self.compose_conversation_notification_header(conversation),
"",
profile.text,
"",
]
lines.extend(self.format_history_message(message) for message in conversation.messages)
if agent := conversation.assigned_agent:
if agent.telegram_user_id:
agent_ref = self._format_telegram_mention(
telegram_user_id=agent.telegram_user_id,
telegram_first_name=agent.telegram_first_name,
telegram_last_name=None, # less formal
telegram_username=agent.telegram_username,
format_=profile.format,
)
else:
logger.warning(f"Can't mention {agent} — unsupported agent frontend")
agent_ref = f"#_{agent.id}"
lines.extend(("", self.conversation_notification_assigned_to.format(agent=agent_ref)))
if conversation.tags:
tags = [self._format_telegram_tag(tag) for tag in conversation.tags]
lines.extend(("", " ".join(tags)))
return Text(text="\n".join(lines), format=profile.format)

def compose_nudge_to_start_bot_notification(
self, agent: Agent, telegram_bot_username: str
) -> Text:
raise NotImplementedError

telegram_assign_to_me_button_text: str

def _format_telegram_mention(
self,
telegram_user_id: int,
telegram_first_name: Optional[str],
telegram_last_name: Optional[str],
telegram_username: Optional[str],
format_: Format,
) -> str:
full_name: str = (
f"{telegram_first_name or ''} {telegram_last_name or ''}".strip()
or telegram_username
or str(telegram_user_id)
)

if format_ == Format.PLAIN:
if not telegram_username:
logger.warning(
f"Can't mention Telegram user {telegram_user_id} without username in plain format"
)
return f"@{telegram_username}" if telegram_username else full_name

url = f"tg://user?id={telegram_user_id}"

if format_ == Format.TELEGRAM_MARKDOWN:
escaped_name = escape_markdown(full_name)
return f"[{escaped_name}]({url})"

if format_ == Format.TELEGRAM_HTML:
escaped_name = html.escape(full_name)
return f'<a href="{url}">{escaped_name}</a>'

raise ValueError(f"text format {format_.value!r} is not supported")

_TAG_REGEX = re.compile(rf"^\s*({EMOJI_SEQUENCE}*)(.*?)({EMOJI_SEQUENCE}*)\s*$")

def _format_telegram_tag(self, tag: ConversationTag) -> str:
Expand Down

0 comments on commit 8f4542d

Please sign in to comment.