Skip to content

Commit ba6df0f

Browse files
authored
Merge pull request game-by-virtuals#97 from game-by-virtuals/plugin/yang-add-telegram-plugin
[Plugin] Add Telegram Plugin
2 parents f6c548d + c41abc8 commit ba6df0f

File tree

7 files changed

+499
-0
lines changed

7 files changed

+499
-0
lines changed

plugins/telegram/README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Telegram Plugin for GAME SDK
2+
3+
## Overview
4+
5+
The **Telegram Plugin** is an integration for the **Game SDK** that enables AI-driven interactions on Telegram. This plugin allows AI agents to handle messages, execute commands, and engage users through text, media, and polls.
6+
7+
## Features
8+
9+
- **Send Messages** – AI agents can send text messages to users.
10+
- **Send Media** – Supports sending photos, documents, videos, and audio.
11+
- **Create Polls** – AI agents can generate interactive polls.
12+
- **Pin & Unpin Messages** – Manage pinned messages in chats.
13+
- **Delete Messages** – Remove messages dynamically.
14+
- **AI-Powered Responses** – Leverages LLM to generate contextual replies.
15+
- **Real-Time Polling** – Runs asynchronously with Telegram’s polling system.
16+
- and more features to come!
17+
18+
## Installation
19+
### Pre-requisites
20+
Ensure you have Python 3.9+ installed. Then, install the plugin via **PyPI**:
21+
### Steps
22+
1. Install the plugin:
23+
```sh bash
24+
pip install telegram-plugin-gamesdk
25+
```
26+
2. Ensure you have a Telegram bot token and GAME API key and set them as environment variables:
27+
```sh bash
28+
export TELEGRAM_BOT_TOKEN="your-telegram-bot-token"
29+
export GAME_API_KEY="your-game-api-key"
30+
```
31+
3. Refer to the example and run the example bot:
32+
```sh bash
33+
python examples/test_telegram.py
34+
```
35+
36+
## Usage Examples
37+
### Initializing the Plugin
38+
39+
```python
40+
from telegram_plugin_gamesdk.telegram_plugin import TelegramPlugin
41+
42+
tg_bot = TelegramPlugin(bot_token='your-telegram-bot-token')
43+
tg_bot.start_polling()
44+
```
45+
46+
### Sending a Message
47+
```python
48+
tg_bot.send_message(chat_id=123456789, text="Hello from the AI Agent!")
49+
```
50+
51+
### Sending Media
52+
```python
53+
tg_bot.send_media(chat_id=123456789, media_type="photo", media="https://example.com/image.jpg", caption="Check this out!")
54+
```
55+
56+
### Creating a Poll
57+
```python
58+
tg_bot.create_poll(chat_id=123456789, question="What's your favorite color?", options=["Red", "Blue", "Green"])
59+
```
60+
61+
### Pinning and Unpinning Messages
62+
```python
63+
tg_bot.pin_message(chat_id=123456789, message_id=42)
64+
tg_bot.unpin_message(chat_id=123456789, message_id=42)
65+
```
66+
67+
### Deleting a Message
68+
```python
69+
tg_bot.delete_message(chat_id=123456789, message_id=42)
70+
```
71+
72+
## Integration with GAME Chat Agent
73+
Implement a message handler to integrate the Telegram Plugin with the GAME Chat Agent:
74+
```python
75+
from telegram import Update
76+
from telegram.ext import ContextTypes, filters, MessageHandler
77+
from game_sdk.game.chat_agent import ChatAgent
78+
79+
chat_agent = ChatAgent(
80+
prompt="You are a helpful assistant.",
81+
api_key="your-game-api-key",
82+
)
83+
84+
async def default_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
85+
"""Handles incoming messages but ignores messages from the bot itself unless it's mentioned in a group chat."""
86+
# Ignore messages from the bot itself
87+
if update.message.from_user.id == tg_plugin.bot.id:
88+
logger.info("Ignoring bot's own message.")
89+
return
90+
91+
user = update.message.from_user
92+
chat_id = update.message.chat.id
93+
chat_type = update.message.chat.type # "private", "group", "supergroup", or "channel"
94+
bot_username = f"@{tg_plugin.bot.username}"
95+
96+
logger.info(f"Update received: {update}")
97+
logger.info(f"Message received: {update.message.text}")
98+
99+
name = f"{user.first_name} (Telegram's chat_id: {chat_id}, this is not part of the partner's name but important for the telegram's function arguments)"
100+
101+
if not any(u["chat_id"] == chat_id for u in active_users):
102+
# Ignore group/supergroup messages unless the bot is mentioned
103+
if chat_type in ["group", "supergroup"] and bot_username not in update.message.text:
104+
logger.info(f"Ignoring group message not mentioning the bot: {update.message.text}")
105+
return
106+
active_users.append({"chat_id": chat_id, "name": name})
107+
logger.info(f"Active user added: {name}")
108+
logger.info(f"Active users: {active_users}")
109+
chat = chat_agent.create_chat(
110+
partner_id=str(chat_id),
111+
partner_name=name,
112+
action_space=agent_action_space,
113+
)
114+
active_chats[chat_id] = chat
115+
116+
response = active_chats[chat_id].next(update.message.text.replace(bot_username, "").strip()) # Remove bot mention
117+
logger.info(f"Response: {response}")
118+
119+
if response.message:
120+
await update.message.reply_text(response.message)
121+
122+
if response.is_finished:
123+
active_chats.pop(chat_id)
124+
active_users.remove({"chat_id": chat_id, "name": name})
125+
logger.info(f"Chat with {name} ended.")
126+
logger.info(f"Active users: {active_users}")
127+
128+
tg_plugin.add_handler(MessageHandler(filters.ALL, default_message_handler))
129+
```
130+
You can refer to [test_telegram.py](examples/test_telegram.py) for details.

plugins/telegram/__init__.py

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import os
2+
from typing import TypedDict
3+
import logging
4+
5+
from telegram import Update
6+
from telegram.ext import ContextTypes, filters, MessageHandler
7+
8+
from game_sdk.game.chat_agent import Chat, ChatAgent
9+
from telegram_plugin_gamesdk.telegram_plugin import TelegramPlugin
10+
from test_telegram_game_functions import send_message_fn, send_media_fn, create_poll_fn, pin_message_fn, unpin_message_fn, delete_message_fn
11+
12+
game_api_key = os.environ.get("GAME_API_KEY")
13+
telegram_bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
14+
15+
logging.basicConfig(
16+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
17+
level=logging.INFO,
18+
)
19+
20+
logger = logging.getLogger(__name__)
21+
22+
class ActiveUser(TypedDict):
23+
chat_id: int
24+
name: str
25+
26+
chat_agent = ChatAgent(
27+
prompt="You are a helpful assistant.",
28+
api_key=game_api_key,
29+
)
30+
31+
active_users: list[ActiveUser] = []
32+
active_chats: dict[int, Chat] = {}
33+
34+
if __name__ == "__main__":
35+
tg_plugin = TelegramPlugin(bot_token=telegram_bot_token)
36+
37+
agent_action_space = [
38+
send_message_fn(tg_plugin),
39+
send_media_fn(tg_plugin),
40+
create_poll_fn(tg_plugin),
41+
pin_message_fn(tg_plugin),
42+
unpin_message_fn(tg_plugin),
43+
delete_message_fn(tg_plugin),
44+
]
45+
46+
async def default_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
47+
"""Handles incoming messages but ignores messages from the bot itself unless it's mentioned in a group chat."""
48+
49+
# Ignore messages from the bot itself
50+
if update.message.from_user.id == tg_plugin.bot.id:
51+
logger.info("Ignoring bot's own message.")
52+
return
53+
54+
user = update.message.from_user
55+
chat_id = update.message.chat.id
56+
chat_type = update.message.chat.type # "private", "group", "supergroup", or "channel"
57+
bot_username = f"@{tg_plugin.bot.username}"
58+
59+
logger.info(f"Update received: {update}")
60+
logger.info(f"Message received: {update.message.text}")
61+
62+
name = f"{user.first_name} (Telegram's chat_id: {chat_id}, this is not part of the partner's name but important for the telegram's function arguments)"
63+
64+
# Ignore group/supergroup messages unless the bot is mentioned
65+
if chat_type in ["group", "supergroup"] and bot_username not in update.message.text:
66+
logger.info(f"Ignoring group message not mentioning the bot: {update.message.text}")
67+
return
68+
69+
if not any(u["chat_id"] == chat_id for u in active_users):
70+
active_users.append({"chat_id": chat_id, "name": name})
71+
logger.info(f"Active user added: {name}")
72+
logger.info(f"Active users: {active_users}")
73+
chat = chat_agent.create_chat(
74+
partner_id=str(chat_id),
75+
partner_name=name,
76+
action_space=agent_action_space,
77+
)
78+
active_chats[chat_id] = chat
79+
80+
response = active_chats[chat_id].next(update.message.text.replace(bot_username, "").strip()) # Remove bot mention
81+
logger.info(f"Response: {response}")
82+
83+
if response.message:
84+
await update.message.reply_text(response.message)
85+
86+
if response.is_finished:
87+
active_chats.pop(chat_id)
88+
active_users.remove({"chat_id": chat_id, "name": name})
89+
logger.info(f"Chat with {name} ended.")
90+
logger.info(f"Active users: {active_users}")
91+
92+
tg_plugin.add_handler(MessageHandler(filters.ALL, default_message_handler))
93+
94+
# Start polling
95+
tg_plugin.start_polling()
96+
97+
# Example of executing a function from Telegram Plugin to a chat without polling
98+
#tg_plugin.send_message(chat_id=829856292, text="Hello! I am a helpful assistant. How can I assist you today?")
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import os
2+
import sys
3+
from typing import Tuple
4+
5+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))
6+
sys.path.append(project_root)
7+
8+
from plugins.telegram.telegram_plugin_gamesdk.telegram_plugin import TelegramPlugin
9+
from src.game_sdk.game.custom_types import Function, FunctionResultStatus, Argument
10+
11+
12+
def send_message_executable(tg_plugin: TelegramPlugin, chat_id: str, text: str) -> Tuple[FunctionResultStatus, str, dict]:
13+
try:
14+
tg_plugin.send_message(chat_id=chat_id, text=text)
15+
return FunctionResultStatus.DONE, "Message sent successfully", {}
16+
except Exception as e:
17+
return FunctionResultStatus.FAILED, str(e), {}
18+
19+
def send_message_fn(bot: TelegramPlugin) -> Function:
20+
return Function(
21+
fn_name="send_message",
22+
fn_description="Send a text message to a Telegram chat",
23+
args=[
24+
Argument(name="chat_id", description="ID of the chat to send the message to", type="str"),
25+
Argument(name="text", description="Text message to send", type="str"),
26+
],
27+
executable=lambda chat_id, text: send_message_executable(bot, chat_id, text),
28+
)
29+
30+
def send_media_executable(tg_plugin: TelegramPlugin, chat_id: str, media_type: str, media: str, caption: str = None) -> Tuple[FunctionResultStatus, str, dict]:
31+
try:
32+
tg_plugin.send_media(chat_id=chat_id, media_type=media_type, media=media, caption=caption)
33+
return FunctionResultStatus.DONE, "Media sent successfully", {}
34+
except Exception as e:
35+
return FunctionResultStatus.FAILED, str(e), {}
36+
37+
def send_media_fn(bot: TelegramPlugin) -> Function:
38+
return Function(
39+
fn_name="send_media",
40+
fn_description="Send a media message to a Telegram chat",
41+
args=[
42+
Argument(name="chat_id", description="ID of the chat to send the message to", type="str"),
43+
Argument(name="media_type", description="Type of media to send (photo, document, video, audio)", type="str"),
44+
Argument(name="media", description="Media URL or file path to send", type="str"),
45+
Argument(name="caption", description="Optional caption for the media", type="str", optional=True),
46+
],
47+
executable=lambda chat_id, media_type, media, caption=None: send_media_executable(bot, chat_id, media_type, media, caption),
48+
)
49+
50+
def create_poll_executable(tg_plugin: TelegramPlugin, chat_id: str, question: str, options: list[str], is_anonymous: bool = True, allows_multiple_answers: bool = False) -> Tuple[FunctionResultStatus, str, dict]:
51+
try:
52+
tg_plugin.create_poll(chat_id=chat_id, question=question, options=options, is_anonymous=is_anonymous, allows_multiple_answers=allows_multiple_answers)
53+
return FunctionResultStatus.DONE, "Poll created successfully", {}
54+
except Exception as e:
55+
return FunctionResultStatus.FAILED, str(e), {}
56+
57+
def create_poll_fn(bot: TelegramPlugin) -> Function:
58+
return Function(
59+
fn_name="create_poll",
60+
fn_description="Create a poll in a Telegram chat",
61+
args=[
62+
Argument(name="chat_id", description="ID of the chat to create the poll in", type="str"),
63+
Argument(name="question", description="Question to ask in the poll", type="str"),
64+
Argument(name="options", description="List of options for the poll", type="List[str]"),
65+
Argument(name="is_anonymous", description="Whether the poll is anonymous (default: True)", type="bool", optional=True),
66+
Argument(name="allows_multiple_answers", description="Whether multiple answers are allowed (default: False)", type="bool", optional=True),
67+
],
68+
executable=lambda chat_id, question, options, is_anonymous=False, allows_multiple_answers=False: create_poll_executable(bot, chat_id, question, options, is_anonymous, allows_multiple_answers),
69+
)
70+
71+
def pin_message_executable(tg_plugin: TelegramPlugin, chat_id: str, message_id: int) -> Tuple[FunctionResultStatus, str, dict]:
72+
try:
73+
tg_plugin.pin_message(chat_id=chat_id, message_id=message_id)
74+
return FunctionResultStatus.DONE, "Message pinned successfully", {}
75+
except Exception as e:
76+
return FunctionResultStatus.FAILED, str(e), {}
77+
78+
def pin_message_fn(bot: TelegramPlugin) -> Function:
79+
return Function(
80+
fn_name="pin_message",
81+
fn_description="Pin a message in a Telegram chat",
82+
args=[
83+
Argument(name="chat_id", description="ID of the chat to pin the message in", type="str"),
84+
Argument(name="message_id", description="ID of the message to pin", type="int"),
85+
],
86+
executable=lambda chat_id, message_id: pin_message_executable(bot, chat_id, message_id),
87+
)
88+
89+
def unpin_message_executable(tg_plugin: TelegramPlugin, chat_id: str, message_id: int) -> Tuple[FunctionResultStatus, str, dict]:
90+
try:
91+
tg_plugin.unpin_message(chat_id=chat_id, message_id=message_id)
92+
return FunctionResultStatus.DONE, "Message unpinned successfully", {}
93+
except Exception as e:
94+
return FunctionResultStatus.FAILED, str(e), {}
95+
96+
def unpin_message_fn(bot: TelegramPlugin) -> Function:
97+
return Function(
98+
fn_name="unpin_message",
99+
fn_description="Unpin a message in a Telegram chat",
100+
args=[
101+
Argument(name="chat_id", description="ID of the chat to unpin the message in", type="str"),
102+
Argument(name="message_id", description="ID of the message to unpin", type="int"),
103+
],
104+
executable=lambda chat_id, message_id: unpin_message_executable(bot, chat_id, message_id),
105+
)
106+
107+
def delete_message_executable(tg_plugin: TelegramPlugin, chat_id: str, message_id: int) -> Tuple[FunctionResultStatus, str, dict]:
108+
try:
109+
tg_plugin.delete_message(chat_id=chat_id, message_id=message_id)
110+
return FunctionResultStatus.DONE, "Message deleted successfully", {}
111+
except Exception as e:
112+
return FunctionResultStatus.FAILED, str(e), {}
113+
114+
def delete_message_fn(bot: TelegramPlugin) -> Function:
115+
return Function(
116+
fn_name="delete_message",
117+
fn_description="Delete a message in a Telegram chat",
118+
args=[
119+
Argument(name="chat_id", description="ID of the chat to delete the message in", type="str"),
120+
Argument(name="message_id", description="ID of the message to delete", type="int"),
121+
],
122+
executable=lambda chat_id, message_id: delete_message_executable(bot, chat_id, message_id),
123+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# General Information
2+
plugin_name: "telegram_plugin_gamesdk"
3+
author: "Yang from Virtuals Protocol"
4+
logo_url: "https://drive.google.com/drive/folders/1AdRKQac0a-ORqdE6mtV4wGdF8y3DhIyo"
5+
release_date: "2025-03"
6+
7+
# Description
8+
short_description: "Telegram Plugin for Game SDK"
9+
detailed_description: "This plugin provides a Telegram integration for the Game SDK. It allows developers to easily integrate Telegram functionalities like sending, deleting, pin, messages or media files etc. Credits to the yk(yenkhoon) for building the Telegram plugin for GAME Node SDK."
10+
11+
# Contact & Support
12+
x_account_handle: "@GAME_Virtuals"
13+
support_contact: "https://discord.gg/virtualsio"
14+
community_link: "https://t.me/virtuals"

plugins/telegram/pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[project]
2+
name = "telegram-plugin-gamesdk"
3+
version = "0.1.0"
4+
description = "Telegram Plugin for Python SDK for GAME by Virtuals"
5+
authors = [
6+
{name = "Ang Weoy Yang", email = "[email protected]"}
7+
]
8+
readme = "README.md"
9+
requires-python = ">=3.9"
10+
dependencies = [
11+
"python-telegram-bot>=21.11.1",
12+
]
13+
14+
[build-system]
15+
requires = ["poetry-core>=2.0.0,<3.0.0"]
16+
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)