telegroq 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of telegroq might be problematic. Click here for more details.
- telegroq-0.1.0/CHANGELOG.md +23 -0
- telegroq-0.1.0/LICENSE +21 -0
- telegroq-0.1.0/PKG-INFO +222 -0
- telegroq-0.1.0/README.md +186 -0
- telegroq-0.1.0/examples/main.py +61 -0
- telegroq-0.1.0/examples/persona_switcher.py +75 -0
- telegroq-0.1.0/pyproject.toml +67 -0
- telegroq-0.1.0/src/telegroq/__init__.py +78 -0
- telegroq-0.1.0/src/telegroq/_groq.py +62 -0
- telegroq-0.1.0/src/telegroq/bot.py +494 -0
- telegroq-0.1.0/src/telegroq/character.py +217 -0
- telegroq-0.1.0/src/telegroq/errors.py +23 -0
- telegroq-0.1.0/src/telegroq/fallbacks.py +53 -0
- telegroq-0.1.0/src/telegroq/keyboards.py +60 -0
- telegroq-0.1.0/src/telegroq/message.py +150 -0
- telegroq-0.1.0/src/telegroq/py.typed +0 -0
- telegroq-0.1.0/src/telegroq/storage.py +108 -0
- telegroq-0.1.0/tests/conftest.py +161 -0
- telegroq-0.1.0/tests/test_bot.py +348 -0
- telegroq-0.1.0/tests/test_callbacks.py +153 -0
- telegroq-0.1.0/tests/test_character.py +201 -0
- telegroq-0.1.0/tests/test_keyboards_and_fallbacks.py +57 -0
- telegroq-0.1.0/tests/test_storage.py +82 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-10-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `telegroq.createBot(token)` returning `TeleGroqBot`, a wrapper around `telebot.TeleBot`.
|
|
11
|
+
- `bot.set_groq_key(api_key, model=...)` and `telegroq.configure_groq(...)`.
|
|
12
|
+
- `telegroq.setCh(system_prompt, max_history)` returning `Character`: per-user memory,
|
|
13
|
+
`get_response` / `ask` (with a `Reply` status object), async variants, `clear_history`.
|
|
14
|
+
- Friendly fallback messages for Groq rate limits, bad keys, timeouts and outages.
|
|
15
|
+
- `@bot.cmd`, `@bot.on_message`, `@bot.callback` (exact / prefix / regex), `@bot.on_error`,
|
|
16
|
+
`bot.auto_reply(character)`; handlers receive `(user, msg)` / `(user, call)`.
|
|
17
|
+
- `Message` and `CallbackContext` wrappers (`msg.text` is always a `str`, `call.edit()`,
|
|
18
|
+
automatic callback answers).
|
|
19
|
+
- Inline keyboard helpers `button()` and `inline_keyboard()`.
|
|
20
|
+
- Pluggable history stores: `MemoryHistoryStore`, `SQLiteHistoryStore`, `HistoryStore` base class.
|
|
21
|
+
- `bot.send_message` / `bot.reply`: automatic splitting of long texts, plain-text retry on
|
|
22
|
+
formatting errors, `reply_markup` on the last part.
|
|
23
|
+
- Command menu publishing (`@bot.cmd("start", description="...")`), non-text message notice.
|
telegroq-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AYM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
telegroq-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: telegroq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deploy a Groq-powered Telegram bot with a custom persona in a few lines (pyTelegramBotAPI + Groq).
|
|
5
|
+
Keywords: telegram,bot,groq,llm,chatbot,ai,persona,pytelegrambotapi,telebot
|
|
6
|
+
Author: AYM
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pyTelegramBotAPI>=4.16
|
|
24
|
+
Requires-Dist: groq>=0.9
|
|
25
|
+
Requires-Dist: pytest>=7.0 ; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.5 ; extra == "dev"
|
|
27
|
+
Requires-Dist: flit>=3.11 ; extra == "dev"
|
|
28
|
+
Requires-Dist: twine>=6.1 ; extra == "dev"
|
|
29
|
+
Project-URL: Changelog, https://github.com/kkkik/telegroq/blob/main/CHANGELOG.md
|
|
30
|
+
Project-URL: Homepage, https://github.com/kkkik/telegroq
|
|
31
|
+
Project-URL: Issues, https://github.com/kkkik/telegroq/issues
|
|
32
|
+
Project-URL: Source, https://github.com/kkkik/telegroq
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Import-Name: telegroq
|
|
35
|
+
|
|
36
|
+
# telegroq
|
|
37
|
+
|
|
38
|
+
**A Groq-powered Telegram bot with its own personality in a few lines of Python.**
|
|
39
|
+
|
|
40
|
+
`telegroq` bridges [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI) (`telebot`) and the
|
|
41
|
+
[Groq API](https://console.groq.com/docs). You get decorators for commands, messages and inline buttons,
|
|
42
|
+
characters with per-user memory, and sensible behaviour when things go wrong (rate limits, bad keys,
|
|
43
|
+
photos and stickers instead of text) - without writing the glue yourself.
|
|
44
|
+
|
|
45
|
+
> Unofficial project. Not affiliated with or endorsed by Telegram or Groq.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install telegroq
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Python 3.10+. You need a Telegram bot token from [@BotFather](https://t.me/BotFather) and a Groq API key from
|
|
54
|
+
[console.groq.com](https://console.groq.com/keys).
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import telegroq
|
|
60
|
+
|
|
61
|
+
bot = telegroq.createBot("123456:TELEGRAM_BOT_TOKEN")
|
|
62
|
+
bot.set_groq_key("gsk_...", model="llama-3.3-70b-versatile")
|
|
63
|
+
|
|
64
|
+
character = telegroq.setCh(
|
|
65
|
+
system_prompt="You are a funny, witty, and energetic AI assistant.",
|
|
66
|
+
max_history=10,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@bot.cmd("/start")
|
|
70
|
+
def start(user, msg):
|
|
71
|
+
bot.send_message(user.id, f"Hello {user.first_name}! I am ready.")
|
|
72
|
+
|
|
73
|
+
@bot.cmd("/reset")
|
|
74
|
+
def reset(user, msg):
|
|
75
|
+
character.clear_history(user.id)
|
|
76
|
+
bot.send_message(user.id, "Conversation memory cleared!")
|
|
77
|
+
|
|
78
|
+
@bot.on_message()
|
|
79
|
+
def chat(user, msg):
|
|
80
|
+
bot.reply(user.id, character.get_response(user.id, msg.text), message_id=msg.message_id)
|
|
81
|
+
|
|
82
|
+
bot.run()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Even shorter - let telegroq answer every text message for you: replace the `chat` handler with
|
|
86
|
+
`bot.auto_reply(character)`.
|
|
87
|
+
|
|
88
|
+
Keep secrets out of your code: read them from environment variables (see [`examples/main.py`](examples/main.py)).
|
|
89
|
+
|
|
90
|
+
## Concepts
|
|
91
|
+
|
|
92
|
+
### Handlers receive `(user, msg)`
|
|
93
|
+
|
|
94
|
+
* `user` is telebot's `User` (`.id`, `.username`, `.first_name`, ...).
|
|
95
|
+
* `msg` is a `telegroq.Message`. `msg.text` is **always a string** (`""` for photos, stickers and voice notes),
|
|
96
|
+
`msg.args` is the text after a command, `msg.chat_id` / `msg.is_private` describe the chat, and
|
|
97
|
+
`msg.reply(text)` answers in the chat the message came from. Everything else (`msg.message_id`,
|
|
98
|
+
`msg.caption`, `msg.photo`, ...) is forwarded to the raw telebot message (`msg.raw`).
|
|
99
|
+
|
|
100
|
+
> **Groups:** `bot.send_message(user.id, ...)` writes to the user's *private* chat. In group chats use
|
|
101
|
+
> `msg.reply(...)` or `bot.send_message(msg.chat_id, ...)`. Restrict AI handlers with
|
|
102
|
+
> `@bot.on_message(chat_types=["private"])` if you do not want the bot to answer everything in a group.
|
|
103
|
+
|
|
104
|
+
### Commands
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
@bot.cmd("/start", description="Say hello") # description appears in Telegram's command menu
|
|
108
|
+
@bot.cmd("help", "about") # several names, slash optional, any letter case
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Registered commands are never delivered to `@bot.on_message()`, whatever order you declare them in.
|
|
112
|
+
|
|
113
|
+
### Characters (personas with memory)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
character = telegroq.setCh("You are a pirate.", max_history=10, temperature=0.9, model="llama-3.1-8b-instant")
|
|
117
|
+
|
|
118
|
+
character.get_response(user.id, "Hi!") # -> str (never raises on Groq errors)
|
|
119
|
+
character.ask(user.id, "Hi!") # -> Reply(text, ok, error_kind, error)
|
|
120
|
+
await character.aget_response(user.id, "Hi") # async variant (runs in a worker thread)
|
|
121
|
+
character.clear_history(user.id)
|
|
122
|
+
character.get_history(user.id)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
* History is kept **per user id** and trimmed to the last `max_history` messages (`0` = stateless).
|
|
126
|
+
Calls for the same user are serialised, so quick successive messages cannot scramble the conversation.
|
|
127
|
+
* `system_prompt` may also be a callable `(user_id) -> str` for per-user prompts.
|
|
128
|
+
* `extra_params={"top_p": 0.9}` is forwarded to `chat.completions.create`.
|
|
129
|
+
* Pass `client=` to use your own Groq-compatible client, or `bot=` / `.bind(bot)` to pick which bot's key is used.
|
|
130
|
+
Without either, the most recently configured key is used.
|
|
131
|
+
|
|
132
|
+
### Inline buttons and callbacks
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from telegroq import inline_keyboard
|
|
136
|
+
|
|
137
|
+
@bot.cmd("persona")
|
|
138
|
+
def choose(user, msg):
|
|
139
|
+
bot.send_message(msg.chat_id, "Who am I?", reply_markup=inline_keyboard(
|
|
140
|
+
[("Pirate", "persona:pirate"), ("Poet", "persona:poet")],
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
@bot.callback(prefix="persona:") # or @bot.callback("exact:data") / @bot.callback(regex=...)
|
|
144
|
+
def picked(user, call):
|
|
145
|
+
call.answer(f"OK, {call.args}!") # optional: telegroq answers for you if you don't
|
|
146
|
+
call.edit(f"Persona: {call.args}") # edit the message that holds the buttons
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`call` is a `CallbackContext` with `.data`, `.args` (data after the prefix), `.message`, `.answer()`,
|
|
150
|
+
`.edit()`, `.edit_keyboard()` and `.reply()`. The button's loading spinner is cleared automatically.
|
|
151
|
+
`button(text, callback_data)` enforces Telegram's 64-byte limit; `button(text, url=...)` makes link buttons.
|
|
152
|
+
|
|
153
|
+
### Persistent memory
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from telegroq import SQLiteHistoryStore
|
|
157
|
+
|
|
158
|
+
character = telegroq.setCh("...", store=SQLiteHistoryStore("history.db"))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Implement `telegroq.HistoryStore` (`load`, `save`, `clear`) to use Redis, Postgres, etc. The default
|
|
162
|
+
`MemoryHistoryStore` forgets everything on restart.
|
|
163
|
+
|
|
164
|
+
### Errors never take the bot down
|
|
165
|
+
|
|
166
|
+
| Situation | What happens |
|
|
167
|
+
|---|---|
|
|
168
|
+
| Groq rate limit, bad key, timeout, outage | The user gets a friendly fallback message; the failed turn is not saved. Customise with `setCh(..., fallbacks={"rate_limit": "..."})`. |
|
|
169
|
+
| Your handler raises | The error is logged, the user gets `bot.error_reply` (or run your own `@bot.on_error`), polling continues. |
|
|
170
|
+
| Photo, sticker, voice note, ... | `msg.text == ""`; in private chats the user gets `bot.non_text_reply` unless you handle that type (`@bot.on_message(content_types=["photo"])`). Set it to `None` to stay silent. |
|
|
171
|
+
| Model output breaks Markdown/HTML | `send_message(..., parse_mode=...)` retries as plain text. |
|
|
172
|
+
| Reply longer than 4096 characters | Split into several messages automatically. |
|
|
173
|
+
| Network errors while polling | `bot.run()` keeps retrying. |
|
|
174
|
+
|
|
175
|
+
Forgetting `set_groq_key` raises `telegroq.ConfigurationError` - a setup bug, not a runtime failure.
|
|
176
|
+
|
|
177
|
+
### Logging
|
|
178
|
+
|
|
179
|
+
telegroq logs to the `telegroq` logger and is silent by default. Enable it with
|
|
180
|
+
`logging.basicConfig(level=logging.INFO)`.
|
|
181
|
+
|
|
182
|
+
## API summary
|
|
183
|
+
|
|
184
|
+
| | |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `telegroq.createBot(token, **telebot_kwargs)` | Create a `TeleGroqBot` (wraps `telebot.TeleBot`, available as `bot.telebot`; unknown attributes forward to it). |
|
|
187
|
+
| `bot.set_groq_key(api_key, model=...)` | Create the Groq client (`timeout`, `max_retries` optional). |
|
|
188
|
+
| `telegroq.setCh(system_prompt, max_history=10, **opts)` | Create a `Character`. |
|
|
189
|
+
| `@bot.cmd(*names, description=None, chat_types=None)` | Command handler `fn(user, msg)`. |
|
|
190
|
+
| `@bot.on_message(content_types=None, chat_types=None, regexp=None, func=None, typing=True)` | Wildcard handler `fn(user, msg)`. |
|
|
191
|
+
| `bot.auto_reply(character, ...)` | Answer every text message with a character. |
|
|
192
|
+
| `@bot.callback(*data, prefix=None, regex=None)` | Inline-button handler `fn(user, call)`. |
|
|
193
|
+
| `@bot.on_error` | `fn(exc, user, update)` called when a handler raises. |
|
|
194
|
+
| `bot.send_message(user_id, text, **kw)` / `bot.reply(user_id, text, message_id=None, **kw)` | Sending helpers (splitting, plain-text retry). |
|
|
195
|
+
| `bot.run()` / `bot.stop()` | Start / stop long polling. |
|
|
196
|
+
| `telegroq.button`, `telegroq.inline_keyboard` | Keyboard helpers. |
|
|
197
|
+
| `HistoryStore`, `MemoryHistoryStore`, `SQLiteHistoryStore` | Conversation storage. |
|
|
198
|
+
|
|
199
|
+
Every public function and class has a docstring; try `help(telegroq.Character)`.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git clone https://github.com/kkkik/telegroq && cd telegroq
|
|
205
|
+
python -m pip install -e ".[dev]"
|
|
206
|
+
pytest # no network or API keys needed (Telegram and Groq are faked)
|
|
207
|
+
ruff check .
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Releasing to PyPI (with flit)
|
|
211
|
+
|
|
212
|
+
1. Bump `__version__` in `src/telegroq/__init__.py` and add a section to `CHANGELOG.md`.
|
|
213
|
+
2. Build and check: `flit build && twine check --strict dist/*`.
|
|
214
|
+
3. Try it on TestPyPI first:
|
|
215
|
+
`FLIT_INDEX_URL=https://test.pypi.org/legacy/ FLIT_USERNAME=__token__ FLIT_PASSWORD=<testpypi-token> flit publish`.
|
|
216
|
+
4. Publish: `FLIT_USERNAME=__token__ FLIT_PASSWORD=<pypi-token> flit publish`, or create a GitHub
|
|
217
|
+
release and let `.github/workflows/publish.yml` publish via PyPI trusted publishing.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT - see [LICENSE](LICENSE).
|
|
222
|
+
|
telegroq-0.1.0/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# telegroq
|
|
2
|
+
|
|
3
|
+
**A Groq-powered Telegram bot with its own personality in a few lines of Python.**
|
|
4
|
+
|
|
5
|
+
`telegroq` bridges [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI) (`telebot`) and the
|
|
6
|
+
[Groq API](https://console.groq.com/docs). You get decorators for commands, messages and inline buttons,
|
|
7
|
+
characters with per-user memory, and sensible behaviour when things go wrong (rate limits, bad keys,
|
|
8
|
+
photos and stickers instead of text) - without writing the glue yourself.
|
|
9
|
+
|
|
10
|
+
> Unofficial project. Not affiliated with or endorsed by Telegram or Groq.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install telegroq
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Python 3.10+. You need a Telegram bot token from [@BotFather](https://t.me/BotFather) and a Groq API key from
|
|
19
|
+
[console.groq.com](https://console.groq.com/keys).
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import telegroq
|
|
25
|
+
|
|
26
|
+
bot = telegroq.createBot("123456:TELEGRAM_BOT_TOKEN")
|
|
27
|
+
bot.set_groq_key("gsk_...", model="llama-3.3-70b-versatile")
|
|
28
|
+
|
|
29
|
+
character = telegroq.setCh(
|
|
30
|
+
system_prompt="You are a funny, witty, and energetic AI assistant.",
|
|
31
|
+
max_history=10,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@bot.cmd("/start")
|
|
35
|
+
def start(user, msg):
|
|
36
|
+
bot.send_message(user.id, f"Hello {user.first_name}! I am ready.")
|
|
37
|
+
|
|
38
|
+
@bot.cmd("/reset")
|
|
39
|
+
def reset(user, msg):
|
|
40
|
+
character.clear_history(user.id)
|
|
41
|
+
bot.send_message(user.id, "Conversation memory cleared!")
|
|
42
|
+
|
|
43
|
+
@bot.on_message()
|
|
44
|
+
def chat(user, msg):
|
|
45
|
+
bot.reply(user.id, character.get_response(user.id, msg.text), message_id=msg.message_id)
|
|
46
|
+
|
|
47
|
+
bot.run()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Even shorter - let telegroq answer every text message for you: replace the `chat` handler with
|
|
51
|
+
`bot.auto_reply(character)`.
|
|
52
|
+
|
|
53
|
+
Keep secrets out of your code: read them from environment variables (see [`examples/main.py`](examples/main.py)).
|
|
54
|
+
|
|
55
|
+
## Concepts
|
|
56
|
+
|
|
57
|
+
### Handlers receive `(user, msg)`
|
|
58
|
+
|
|
59
|
+
* `user` is telebot's `User` (`.id`, `.username`, `.first_name`, ...).
|
|
60
|
+
* `msg` is a `telegroq.Message`. `msg.text` is **always a string** (`""` for photos, stickers and voice notes),
|
|
61
|
+
`msg.args` is the text after a command, `msg.chat_id` / `msg.is_private` describe the chat, and
|
|
62
|
+
`msg.reply(text)` answers in the chat the message came from. Everything else (`msg.message_id`,
|
|
63
|
+
`msg.caption`, `msg.photo`, ...) is forwarded to the raw telebot message (`msg.raw`).
|
|
64
|
+
|
|
65
|
+
> **Groups:** `bot.send_message(user.id, ...)` writes to the user's *private* chat. In group chats use
|
|
66
|
+
> `msg.reply(...)` or `bot.send_message(msg.chat_id, ...)`. Restrict AI handlers with
|
|
67
|
+
> `@bot.on_message(chat_types=["private"])` if you do not want the bot to answer everything in a group.
|
|
68
|
+
|
|
69
|
+
### Commands
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
@bot.cmd("/start", description="Say hello") # description appears in Telegram's command menu
|
|
73
|
+
@bot.cmd("help", "about") # several names, slash optional, any letter case
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Registered commands are never delivered to `@bot.on_message()`, whatever order you declare them in.
|
|
77
|
+
|
|
78
|
+
### Characters (personas with memory)
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
character = telegroq.setCh("You are a pirate.", max_history=10, temperature=0.9, model="llama-3.1-8b-instant")
|
|
82
|
+
|
|
83
|
+
character.get_response(user.id, "Hi!") # -> str (never raises on Groq errors)
|
|
84
|
+
character.ask(user.id, "Hi!") # -> Reply(text, ok, error_kind, error)
|
|
85
|
+
await character.aget_response(user.id, "Hi") # async variant (runs in a worker thread)
|
|
86
|
+
character.clear_history(user.id)
|
|
87
|
+
character.get_history(user.id)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
* History is kept **per user id** and trimmed to the last `max_history` messages (`0` = stateless).
|
|
91
|
+
Calls for the same user are serialised, so quick successive messages cannot scramble the conversation.
|
|
92
|
+
* `system_prompt` may also be a callable `(user_id) -> str` for per-user prompts.
|
|
93
|
+
* `extra_params={"top_p": 0.9}` is forwarded to `chat.completions.create`.
|
|
94
|
+
* Pass `client=` to use your own Groq-compatible client, or `bot=` / `.bind(bot)` to pick which bot's key is used.
|
|
95
|
+
Without either, the most recently configured key is used.
|
|
96
|
+
|
|
97
|
+
### Inline buttons and callbacks
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from telegroq import inline_keyboard
|
|
101
|
+
|
|
102
|
+
@bot.cmd("persona")
|
|
103
|
+
def choose(user, msg):
|
|
104
|
+
bot.send_message(msg.chat_id, "Who am I?", reply_markup=inline_keyboard(
|
|
105
|
+
[("Pirate", "persona:pirate"), ("Poet", "persona:poet")],
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
@bot.callback(prefix="persona:") # or @bot.callback("exact:data") / @bot.callback(regex=...)
|
|
109
|
+
def picked(user, call):
|
|
110
|
+
call.answer(f"OK, {call.args}!") # optional: telegroq answers for you if you don't
|
|
111
|
+
call.edit(f"Persona: {call.args}") # edit the message that holds the buttons
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`call` is a `CallbackContext` with `.data`, `.args` (data after the prefix), `.message`, `.answer()`,
|
|
115
|
+
`.edit()`, `.edit_keyboard()` and `.reply()`. The button's loading spinner is cleared automatically.
|
|
116
|
+
`button(text, callback_data)` enforces Telegram's 64-byte limit; `button(text, url=...)` makes link buttons.
|
|
117
|
+
|
|
118
|
+
### Persistent memory
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from telegroq import SQLiteHistoryStore
|
|
122
|
+
|
|
123
|
+
character = telegroq.setCh("...", store=SQLiteHistoryStore("history.db"))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Implement `telegroq.HistoryStore` (`load`, `save`, `clear`) to use Redis, Postgres, etc. The default
|
|
127
|
+
`MemoryHistoryStore` forgets everything on restart.
|
|
128
|
+
|
|
129
|
+
### Errors never take the bot down
|
|
130
|
+
|
|
131
|
+
| Situation | What happens |
|
|
132
|
+
|---|---|
|
|
133
|
+
| Groq rate limit, bad key, timeout, outage | The user gets a friendly fallback message; the failed turn is not saved. Customise with `setCh(..., fallbacks={"rate_limit": "..."})`. |
|
|
134
|
+
| Your handler raises | The error is logged, the user gets `bot.error_reply` (or run your own `@bot.on_error`), polling continues. |
|
|
135
|
+
| Photo, sticker, voice note, ... | `msg.text == ""`; in private chats the user gets `bot.non_text_reply` unless you handle that type (`@bot.on_message(content_types=["photo"])`). Set it to `None` to stay silent. |
|
|
136
|
+
| Model output breaks Markdown/HTML | `send_message(..., parse_mode=...)` retries as plain text. |
|
|
137
|
+
| Reply longer than 4096 characters | Split into several messages automatically. |
|
|
138
|
+
| Network errors while polling | `bot.run()` keeps retrying. |
|
|
139
|
+
|
|
140
|
+
Forgetting `set_groq_key` raises `telegroq.ConfigurationError` - a setup bug, not a runtime failure.
|
|
141
|
+
|
|
142
|
+
### Logging
|
|
143
|
+
|
|
144
|
+
telegroq logs to the `telegroq` logger and is silent by default. Enable it with
|
|
145
|
+
`logging.basicConfig(level=logging.INFO)`.
|
|
146
|
+
|
|
147
|
+
## API summary
|
|
148
|
+
|
|
149
|
+
| | |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `telegroq.createBot(token, **telebot_kwargs)` | Create a `TeleGroqBot` (wraps `telebot.TeleBot`, available as `bot.telebot`; unknown attributes forward to it). |
|
|
152
|
+
| `bot.set_groq_key(api_key, model=...)` | Create the Groq client (`timeout`, `max_retries` optional). |
|
|
153
|
+
| `telegroq.setCh(system_prompt, max_history=10, **opts)` | Create a `Character`. |
|
|
154
|
+
| `@bot.cmd(*names, description=None, chat_types=None)` | Command handler `fn(user, msg)`. |
|
|
155
|
+
| `@bot.on_message(content_types=None, chat_types=None, regexp=None, func=None, typing=True)` | Wildcard handler `fn(user, msg)`. |
|
|
156
|
+
| `bot.auto_reply(character, ...)` | Answer every text message with a character. |
|
|
157
|
+
| `@bot.callback(*data, prefix=None, regex=None)` | Inline-button handler `fn(user, call)`. |
|
|
158
|
+
| `@bot.on_error` | `fn(exc, user, update)` called when a handler raises. |
|
|
159
|
+
| `bot.send_message(user_id, text, **kw)` / `bot.reply(user_id, text, message_id=None, **kw)` | Sending helpers (splitting, plain-text retry). |
|
|
160
|
+
| `bot.run()` / `bot.stop()` | Start / stop long polling. |
|
|
161
|
+
| `telegroq.button`, `telegroq.inline_keyboard` | Keyboard helpers. |
|
|
162
|
+
| `HistoryStore`, `MemoryHistoryStore`, `SQLiteHistoryStore` | Conversation storage. |
|
|
163
|
+
|
|
164
|
+
Every public function and class has a docstring; try `help(telegroq.Character)`.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git clone https://github.com/kkkik/telegroq && cd telegroq
|
|
170
|
+
python -m pip install -e ".[dev]"
|
|
171
|
+
pytest # no network or API keys needed (Telegram and Groq are faked)
|
|
172
|
+
ruff check .
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Releasing to PyPI (with flit)
|
|
176
|
+
|
|
177
|
+
1. Bump `__version__` in `src/telegroq/__init__.py` and add a section to `CHANGELOG.md`.
|
|
178
|
+
2. Build and check: `flit build && twine check --strict dist/*`.
|
|
179
|
+
3. Try it on TestPyPI first:
|
|
180
|
+
`FLIT_INDEX_URL=https://test.pypi.org/legacy/ FLIT_USERNAME=__token__ FLIT_PASSWORD=<testpypi-token> flit publish`.
|
|
181
|
+
4. Publish: `FLIT_USERNAME=__token__ FLIT_PASSWORD=<pypi-token> flit publish`, or create a GitHub
|
|
182
|
+
release and let `.github/workflows/publish.yml` publish via PyPI trusted publishing.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT - see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Minimal Groq-powered Telegram bot with a persona.
|
|
2
|
+
|
|
3
|
+
Setup:
|
|
4
|
+
pip install telegroq
|
|
5
|
+
export TELEGRAM_BOT_TOKEN="123456:ABC..." # from @BotFather
|
|
6
|
+
export GROQ_API_KEY="gsk_..." # from console.groq.com
|
|
7
|
+
python main.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import telegroq
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
17
|
+
|
|
18
|
+
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
|
|
19
|
+
GROQ_KEY = os.environ.get("GROQ_API_KEY")
|
|
20
|
+
if not TELEGRAM_TOKEN or not GROQ_KEY:
|
|
21
|
+
sys.exit("Set TELEGRAM_BOT_TOKEN and GROQ_API_KEY first (see the top of this file).")
|
|
22
|
+
|
|
23
|
+
# 1. Initialize the bot
|
|
24
|
+
bot = telegroq.createBot(TELEGRAM_TOKEN)
|
|
25
|
+
bot.set_groq_key(GROQ_KEY, model="llama-3.3-70b-versatile")
|
|
26
|
+
|
|
27
|
+
# 2. Define the AI persona (remembers the last 10 messages per user)
|
|
28
|
+
character = telegroq.setCh(
|
|
29
|
+
system_prompt=(
|
|
30
|
+
"You are a funny, witty, and energetic AI assistant. "
|
|
31
|
+
"Keep answers short and punchy, and use plain text (no Markdown)."
|
|
32
|
+
),
|
|
33
|
+
max_history=10,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# 3. Custom commands (the description shows up in Telegram's command menu)
|
|
38
|
+
@bot.cmd("/start", description="Say hello")
|
|
39
|
+
def start(user, msg):
|
|
40
|
+
bot.send_message(user.id, f"Hello {user.first_name}! I am ready. Send me anything.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@bot.cmd("/reset", description="Forget our conversation")
|
|
44
|
+
def reset(user, msg):
|
|
45
|
+
character.clear_history(user.id)
|
|
46
|
+
bot.send_message(user.id, "Conversation memory cleared!")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# 4. AI auto-responder: every text message that is not a command ends up here.
|
|
50
|
+
# Photos, stickers and voice notes get a polite "text only" notice automatically.
|
|
51
|
+
@bot.on_message()
|
|
52
|
+
def chat_handler(user, msg):
|
|
53
|
+
reply = character.get_response(user.id, msg.text)
|
|
54
|
+
bot.reply(user.id, reply, message_id=msg.message_id)
|
|
55
|
+
|
|
56
|
+
# Shorter alternative: replace this whole handler with bot.auto_reply(character)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# 5. Launch (Ctrl+C to stop)
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
bot.run()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Inline buttons choose the persona; conversations survive restarts (SQLite).
|
|
2
|
+
|
|
3
|
+
Setup:
|
|
4
|
+
pip install telegroq
|
|
5
|
+
export TELEGRAM_BOT_TOKEN="123456:ABC..."
|
|
6
|
+
export GROQ_API_KEY="gsk_..."
|
|
7
|
+
python persona_switcher.py
|
|
8
|
+
|
|
9
|
+
Try /persona, press a button, then chat.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
import telegroq
|
|
17
|
+
from telegroq import SQLiteHistoryStore, inline_keyboard
|
|
18
|
+
|
|
19
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
20
|
+
|
|
21
|
+
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
|
|
22
|
+
GROQ_KEY = os.environ.get("GROQ_API_KEY")
|
|
23
|
+
if not TELEGRAM_TOKEN or not GROQ_KEY:
|
|
24
|
+
sys.exit("Set TELEGRAM_BOT_TOKEN and GROQ_API_KEY first (see the top of this file).")
|
|
25
|
+
|
|
26
|
+
bot = telegroq.createBot(TELEGRAM_TOKEN)
|
|
27
|
+
bot.set_groq_key(GROQ_KEY)
|
|
28
|
+
|
|
29
|
+
PROMPTS = {
|
|
30
|
+
"pirate": "You are a swashbuckling pirate. Answer in pirate speak, briefly.",
|
|
31
|
+
"teacher": "You are a patient teacher. Explain step by step, in simple words.",
|
|
32
|
+
"poet": "You are a poet. Reply in a short rhyming verse.",
|
|
33
|
+
}
|
|
34
|
+
# One character per persona; one SQLite file, one table per persona.
|
|
35
|
+
personas = {
|
|
36
|
+
name: telegroq.setCh(prompt, max_history=12, store=SQLiteHistoryStore("history.db", table=name))
|
|
37
|
+
for name, prompt in PROMPTS.items()
|
|
38
|
+
}
|
|
39
|
+
chosen: dict[int, str] = {} # user id -> persona name (use a database for real deployments)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@bot.cmd("start", description="Start the bot")
|
|
43
|
+
@bot.cmd("persona", description="Choose who I am")
|
|
44
|
+
def persona_menu(user, msg):
|
|
45
|
+
keyboard = inline_keyboard([(name.title(), f"persona:{name}") for name in personas])
|
|
46
|
+
bot.send_message(msg.chat_id, "Who should I be?", reply_markup=keyboard)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@bot.callback(prefix="persona:")
|
|
50
|
+
def persona_chosen(user, call):
|
|
51
|
+
chosen[user.id] = call.args # e.g. "persona:pirate" -> "pirate"
|
|
52
|
+
call.answer(f"Now chatting with the {call.args}!")
|
|
53
|
+
call.edit(f"Persona: {call.args.title()}. Say something!")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@bot.cmd("reset", description="Forget our conversation")
|
|
57
|
+
def reset(user, msg):
|
|
58
|
+
personas[chosen.get(user.id, "pirate")].clear_history(user.id)
|
|
59
|
+
msg.reply("Memory cleared.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@bot.on_message(chat_types=["private"])
|
|
63
|
+
def chat(user, msg):
|
|
64
|
+
character = personas[chosen.get(user.id, "pirate")]
|
|
65
|
+
msg.reply(character.get_response(user.id, msg.text))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@bot.on_error
|
|
69
|
+
def log_errors(exc, user, update):
|
|
70
|
+
logging.getLogger(__name__).error("handler failed for user %s: %r", user.id, exc)
|
|
71
|
+
update.reply("Sorry, that went wrong. Try again?") # works for messages and button presses
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
bot.run()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core>=3.11,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "telegroq"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Deploy a Groq-powered Telegram bot with a custom persona in a few lines (pyTelegramBotAPI + Groq)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "AYM" }]
|
|
14
|
+
keywords = ["telegram", "bot", "groq", "llm", "chatbot", "ai", "persona", "pytelegrambotapi", "telebot"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Communications :: Chat",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pyTelegramBotAPI>=4.16",
|
|
31
|
+
"groq>=0.9",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7.0",
|
|
37
|
+
"ruff>=0.5",
|
|
38
|
+
"flit>=3.11",
|
|
39
|
+
"twine>=6.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/kkkik/telegroq"
|
|
44
|
+
Source = "https://github.com/kkkik/telegroq"
|
|
45
|
+
Issues = "https://github.com/kkkik/telegroq/issues"
|
|
46
|
+
Changelog = "https://github.com/kkkik/telegroq/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[tool.flit.module]
|
|
49
|
+
name = "telegroq" # found in src/telegroq; version is read from __version__ in src/telegroq/__init__.py
|
|
50
|
+
|
|
51
|
+
[tool.flit.sdist]
|
|
52
|
+
include = ["tests/", "examples/", "CHANGELOG.md"]
|
|
53
|
+
exclude = ["**/__pycache__/", "**/*.pyc", "publish.sh", "token.txt"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
pythonpath = ["src"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 120
|
|
61
|
+
target-version = "py310"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = ["E", "F", "W"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
"tests/*" = ["E501"]
|