jims-telegram 0.1.0.dev4__py3-none-any.whl

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.
@@ -0,0 +1,270 @@
1
+ import asyncio
2
+ import uuid
3
+ from contextlib import suppress
4
+ from typing import Any, Awaitable, overload
5
+
6
+ from aiogram import Bot, Dispatcher, F
7
+ from aiogram.filters import CommandStart
8
+ from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
9
+ from jims_core.app import JimsApp
10
+ from jims_core.schema import Pipeline
11
+ from jims_core.thread.thread_context import StatusUpdater
12
+ from jims_core.thread.thread_controller import ThreadController
13
+ from jims_core.util import uuid7
14
+ from loguru import logger
15
+ from opentelemetry import trace
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+ from typing_extensions import TypedDict
18
+
19
+ from jims_telegram.md2tgmd import escape
20
+
21
+ tracer = trace.get_tracer("jims_tui")
22
+
23
+
24
+ class TelegramSettings(BaseSettings):
25
+ model_config = SettingsConfigDict(
26
+ env_prefix="TELEGRAM_",
27
+ env_file=".env",
28
+ env_file_encoding="utf-8",
29
+ extra="ignore",
30
+ )
31
+
32
+ bot_token: str
33
+
34
+
35
+ settings = TelegramSettings() # type: ignore
36
+
37
+ uuid_namespace = uuid.UUID("00000000-0000-0000-0000-000000000000")
38
+
39
+
40
+ def uuid_from_int(value: int) -> uuid.UUID:
41
+ if not 0 <= value < 2**64:
42
+ raise ValueError(f"Integer value must be between 0 and 2^64-1, got {value}")
43
+
44
+ # Convert the integer to bytes in big-endian order
45
+ name = value.to_bytes(8, byteorder="big")
46
+
47
+ # Generate a version 5 UUID
48
+ return uuid.uuid5(uuid_namespace, name)
49
+
50
+
51
+ class TelegramStatusUpdater(StatusUpdater):
52
+ def __init__(self, bot: Bot, chat_id: int) -> None:
53
+ self.bot = bot
54
+ self.chat_id = chat_id
55
+
56
+ async def update_status(self, status: str) -> None:
57
+ await self.bot.send_chat_action(
58
+ chat_id=self.chat_id,
59
+ action="typing",
60
+ )
61
+ logger.debug(f"Status updated to: {status}")
62
+
63
+
64
+ class TelegramButton(TypedDict):
65
+ text: str
66
+ id: str
67
+
68
+
69
+ class TelegramController:
70
+ def __init__(
71
+ self,
72
+ app: JimsApp,
73
+ ) -> None:
74
+ self.app = app
75
+ self.bot = Bot(token=settings.bot_token)
76
+
77
+ self.dispatcher = Dispatcher()
78
+
79
+ self.dispatcher.message.register(self.command_start, CommandStart())
80
+ self.dispatcher.message.register(self.handle_message)
81
+ self.dispatcher.callback_query.register(self.handle_callback, F.data.startswith("btn:"))
82
+
83
+ @overload
84
+ @classmethod
85
+ async def create(cls, app: JimsApp) -> "TelegramController":
86
+ ...
87
+
88
+ @overload
89
+ @classmethod
90
+ async def create(cls, app: Awaitable[JimsApp]) -> "TelegramController":
91
+ ...
92
+
93
+ @classmethod
94
+ async def create(cls, app: JimsApp | Awaitable[JimsApp]) -> "TelegramController":
95
+ if isinstance(app, Awaitable):
96
+ app = await app
97
+ return cls(app)
98
+
99
+ async def _run_pipeline(self, ctl: ThreadController, chat_id: Any, pipeline: Pipeline) -> None:
100
+ ctx = await ctl.make_context()
101
+ ctx = ctx.with_status_updater(TelegramStatusUpdater(self.bot, chat_id))
102
+
103
+ async def status_updater():
104
+ try:
105
+ while True:
106
+ await ctx.update_agent_status("thinking")
107
+ await asyncio.sleep(5)
108
+ except asyncio.CancelledError:
109
+ pass
110
+
111
+ updater_task = asyncio.create_task(status_updater())
112
+
113
+ try:
114
+ events = await ctl.run_pipeline_with_context(pipeline, ctx)
115
+ finally:
116
+ updater_task.cancel()
117
+ with suppress(asyncio.CancelledError):
118
+ await updater_task
119
+
120
+ for event in events:
121
+ if event.event_type == "comm.assistant_message":
122
+ await self.bot.send_message(
123
+ chat_id=chat_id,
124
+ text=escape(event.event_data["content"]),
125
+ parse_mode="MarkdownV2",
126
+ )
127
+ elif event.event_type == "comm.assistant_buttons":
128
+ buttons = event.event_data.get("buttons", [])
129
+ reply_markup = self._build_inline_keyboard(buttons)
130
+ await self.bot.send_message(
131
+ chat_id=chat_id,
132
+ text=escape(event.event_data.get("content", "")),
133
+ parse_mode="MarkdownV2",
134
+ reply_markup=reply_markup,
135
+ )
136
+
137
+ async def command_start(self, message: Message) -> None:
138
+ logger.debug(f"Received command start from {message.chat.id}")
139
+ if not message.from_user: # messages sent on behalf of chats, channels, or by tg
140
+ logger.error("from_user.id not found in message")
141
+ return
142
+
143
+ from_id = message.from_user.id # type: ignore[union-attr]
144
+
145
+ thread_id = uuid7()
146
+ ctl = await self.app.new_thread(
147
+ contact_id=f"telegram:{from_id}",
148
+ thread_id=thread_id,
149
+ thread_config={
150
+ "interface": "telegram",
151
+ "telegram_chat_id": message.chat.id,
152
+ "telegram_user_id": from_id,
153
+ "telegram_user_name": message.from_user.username, # type: ignore[union-attr]
154
+ },
155
+ )
156
+
157
+ if message.text:
158
+ await ctl.store_user_message(
159
+ event_id=uuid_from_int(message.message_id),
160
+ content=message.text,
161
+ )
162
+
163
+ if self.app.conversation_start_pipeline is not None:
164
+ await self._run_pipeline(ctl, message.chat.id, self.app.conversation_start_pipeline)
165
+
166
+ async def handle_message(self, message: Message) -> None:
167
+ with tracer.start_as_current_span("jims_telegram.handle_message"):
168
+ logger.debug(f"Received message {message.text=} from {message.chat.id=}")
169
+ if not message.from_user: # messages sent on behalf of chats, channels, or by tg
170
+ logger.error("from_user.id not found in message")
171
+ return
172
+
173
+ from_id = message.from_user.id # type: ignore[union-attr]
174
+
175
+ ctl = await ThreadController.latest_thread_from_contact_id(self.app.sessionmaker, f"telegram:{from_id}")
176
+ if ctl is None:
177
+ logger.warning(f"Thread with id {message.chat.id} not found, recreating")
178
+ thread_id = uuid7()
179
+ ctl = await ThreadController.new_thread(
180
+ self.app.sessionmaker,
181
+ contact_id=f"telegram:{from_id}",
182
+ thread_id=thread_id,
183
+ thread_config={
184
+ "interface": "telegram",
185
+ "telegram_chat_id": message.chat.id,
186
+ "telegram_user_id": from_id,
187
+ "telegram_user_name": message.from_user.username, # type: ignore[union-attr]
188
+ },
189
+ )
190
+
191
+ if message.text:
192
+ await ctl.store_user_message(
193
+ event_id=uuid_from_int(message.message_id),
194
+ content=message.text,
195
+ )
196
+
197
+ await self._run_pipeline(ctl, message.chat.id, self.app.pipeline)
198
+
199
+ async def handle_callback(self, callback: CallbackQuery) -> None:
200
+ if not callback.from_user:
201
+ return
202
+ from_id = callback.from_user.id # type: ignore[union-attr]
203
+
204
+ ctl = await ThreadController.latest_thread_from_contact_id(self.app.sessionmaker, f"telegram:{from_id}")
205
+ if ctl is None:
206
+ thread_id = uuid7()
207
+ ctl = await ThreadController.new_thread(
208
+ self.app.sessionmaker,
209
+ contact_id=f"telegram:{from_id}",
210
+ thread_id=thread_id,
211
+ thread_config={
212
+ "interface": "telegram",
213
+ "telegram_chat_id": callback.message.chat.id if callback.message else None,
214
+ "telegram_user_id": from_id,
215
+ "telegram_user_name": callback.message.from_user.username, # type: ignore[union-attr]
216
+ },
217
+ )
218
+
219
+ data = callback.data or ""
220
+ try:
221
+ await ctl.store_event_dict(
222
+ event_id=uuid7(),
223
+ event_type="comm.user_button_click",
224
+ event_data={
225
+ "role": "user",
226
+ "content": data,
227
+ "message_id": callback.message.message_id if callback.message else None,
228
+ },
229
+ )
230
+ except Exception as e:
231
+ logger.error(f"Failed to store button click event: {e}")
232
+
233
+ with suppress(Exception):
234
+ await callback.answer()
235
+
236
+ chat_id = callback.message.chat.id if callback.message else from_id
237
+ await self._run_pipeline(ctl, chat_id, self.app.pipeline)
238
+
239
+ @staticmethod
240
+ def _build_inline_keyboard(
241
+ buttons: list[list[TelegramButton]] | list[TelegramButton] | list[Any],
242
+ ) -> InlineKeyboardMarkup | None:
243
+ """
244
+ Parse buttons dict into keyboard
245
+ """
246
+ rows: list[list[InlineKeyboardButton]] = []
247
+ if buttons and isinstance(buttons, list):
248
+ # normalize single row form
249
+ if buttons and all(isinstance(b, dict) for b in buttons):
250
+ buttons = [buttons] # type: ignore[assignment]
251
+ for row in buttons:
252
+ if not isinstance(row, list):
253
+ continue
254
+ kb_row: list[InlineKeyboardButton] = []
255
+ for b in row:
256
+ if not isinstance(b, dict):
257
+ continue
258
+ label_v = b.get("text")
259
+ if label_v is None:
260
+ continue
261
+ label = str(label_v)
262
+ btn_id = str(b.get("id") or b.get("callback_data") or label)
263
+ kb_row.append(InlineKeyboardButton(text=label, callback_data=f"btn:{btn_id}"))
264
+ if kb_row:
265
+ rows.append(kb_row)
266
+ return InlineKeyboardMarkup(inline_keyboard=rows) if rows else None
267
+
268
+ async def run(self):
269
+ logger.debug("Starting Telegram bot")
270
+ await self.dispatcher.start_polling(self.bot)
jims_telegram/main.py ADDED
@@ -0,0 +1,77 @@
1
+ import asyncio
2
+
3
+ import click
4
+ from aiohttp import web
5
+ from jims_core.util import (
6
+ load_jims_app,
7
+ setup_monitoring_and_tracing_with_sentry,
8
+ setup_prometheus_metrics,
9
+ setup_verbose_logging,
10
+ )
11
+ from loguru import logger
12
+
13
+ from jims_telegram import TelegramController
14
+
15
+
16
+ async def healthcheck(host: str = "0.0.0.0", port: int = 8000):
17
+ async def health(request: web.Request):
18
+ return web.json_response({"status": "ok"})
19
+
20
+ app = web.Application()
21
+ app.add_routes([web.get("/health", health)])
22
+ app.add_routes([web.get("/healthz", health)])
23
+
24
+ runner = web.AppRunner(app)
25
+ await runner.setup()
26
+ site = web.TCPSite(runner, host, port)
27
+ await site.start()
28
+ return runner
29
+
30
+
31
+ @click.command()
32
+ @click.option("--app", type=click.STRING, default="app")
33
+ @click.option("--enable-sentry", is_flag=True, help="Enable tracing to Sentry", default=False)
34
+ @click.option("--enable-healthcheck", is_flag=True, help="Enable healthcheck endpoint", default=True)
35
+ @click.option("--healthcheck-port", type=click.INT, default=9000)
36
+ @click.option("--metrics-port", type=click.INT, default=8000)
37
+ @click.option("--verbose", is_flag=True, default=False)
38
+ def cli(
39
+ app: str,
40
+ enable_sentry: bool,
41
+ enable_healthcheck: bool,
42
+ healthcheck_port: int,
43
+ metrics_port: int,
44
+ verbose: bool,
45
+ ) -> None:
46
+ if verbose:
47
+ setup_verbose_logging()
48
+
49
+ setup_prometheus_metrics(port=metrics_port)
50
+
51
+ if enable_sentry:
52
+ setup_monitoring_and_tracing_with_sentry()
53
+
54
+ jims_app = load_jims_app(app)
55
+
56
+ async def run_telegram_bot():
57
+ if enable_healthcheck:
58
+ await healthcheck(port=healthcheck_port)
59
+
60
+ telegram_controller = await TelegramController.create(jims_app)
61
+ await telegram_controller.run()
62
+
63
+ try:
64
+ asyncio.run(run_telegram_bot())
65
+ except KeyboardInterrupt:
66
+ logger.info("Bot stopped by user")
67
+ except Exception as e:
68
+ logger.error(f"Bot crashed: {e}")
69
+ exit(1)
70
+
71
+
72
+ def main():
73
+ cli(auto_envvar_prefix="JIMS")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,353 @@
1
+ """
2
+ conveniently borrowed from https://github.com/yym68686/md2tgmd
3
+ """
4
+
5
+ import re
6
+
7
+
8
+ def find_all_index(str, pattern):
9
+ index_list = [0]
10
+ for match in re.finditer(pattern, str, re.MULTILINE):
11
+ if match.group(1) is not None:
12
+ start = match.start(1)
13
+ end = match.end(1)
14
+ index_list += [start, end]
15
+ index_list.append(len(str))
16
+ return index_list
17
+
18
+
19
+ def replace_all(text, pattern, function):
20
+ poslist = [0]
21
+ strlist = []
22
+ originstr = []
23
+ poslist = find_all_index(text, pattern)
24
+ for i in range(1, len(poslist[:-1]), 2):
25
+ start, end = poslist[i : i + 2]
26
+ strlist.append(function(text[start:end]))
27
+ for i in range(0, len(poslist), 2):
28
+ j, k = poslist[i : i + 2]
29
+ originstr.append(text[j:k])
30
+ if len(strlist) < len(originstr):
31
+ strlist.append("")
32
+ else:
33
+ originstr.append("")
34
+ new_list = [item for pair in zip(originstr, strlist) for item in pair]
35
+ return "".join(new_list)
36
+
37
+
38
+ def escapeshape(text):
39
+ return "▎*" + " ".join(text.split()[1:]) + "*\n\n"
40
+
41
+
42
+ def escapeminus(text):
43
+ return "\\" + text
44
+
45
+
46
+ def escapeminus2(text):
47
+ return r"@+>@"
48
+
49
+
50
+ def escapebackquote(text):
51
+ return r"\`\`"
52
+
53
+
54
+ def escapebackquoteincode(text):
55
+ return r"@->@"
56
+
57
+
58
+ def escapeplus(text):
59
+ return "\\" + text
60
+
61
+
62
+ def escape_all_backquote(text):
63
+ return "\\" + text
64
+
65
+
66
+ def dedent_space(text):
67
+ import textwrap
68
+
69
+ return "\n\n" + textwrap.dedent(text).strip() + "\n\n"
70
+
71
+
72
+ def split_code(text):
73
+ split_list = []
74
+ if len(text) > 2300:
75
+ split_str_list = text.split("\n\n")
76
+
77
+ conversation_len = len(split_str_list)
78
+ message_index = 1
79
+ while message_index < conversation_len:
80
+ if split_str_list[message_index].startswith(" "):
81
+ split_str_list[message_index - 1] += "\n\n" + split_str_list[message_index]
82
+ split_str_list.pop(message_index)
83
+ conversation_len = conversation_len - 1
84
+ else:
85
+ message_index = message_index + 1
86
+
87
+ split_index = 0
88
+ for index, _ in enumerate(split_str_list):
89
+ if len("".join(split_str_list[:index])) < len(text) // 2:
90
+ split_index += 1
91
+ continue
92
+ else:
93
+ break
94
+ str1 = "\n\n".join(split_str_list[:split_index])
95
+ if not str1.strip().endswith("```"):
96
+ str1 = str1 + "\n```"
97
+ split_list.append(str1)
98
+ code_type = text.split("\n")[0]
99
+ str2 = "\n\n".join(split_str_list[split_index:])
100
+ str2 = code_type + "\n" + str2
101
+ if not str2.strip().endswith("```"):
102
+ str2 = str2 + "\n```"
103
+ split_list.append(str2)
104
+ else:
105
+ split_list.append(text)
106
+
107
+ if len(split_list) > 1:
108
+ split_list = "\n@|@|@|@\n\n".join(split_list)
109
+ else:
110
+ split_list = split_list[0]
111
+ return split_list
112
+
113
+
114
+ def find_lines_with_char(s, char, min_count):
115
+ """
116
+ Find lines containing a specific character at least min_count times.
117
+
118
+ Args:
119
+ s (str): String to process
120
+ char (str): Character to count
121
+ min_count (int): Minimum occurrence count
122
+
123
+ Returns:
124
+ str: String with escaped characters
125
+ """
126
+ lines = s.split("\n") # Split string by lines
127
+
128
+ for index, line in enumerate(lines):
129
+ if re.sub(r"```", "", line).count(char) % 2 != 0 or (
130
+ not line.strip().startswith("```") and line.count(char) % 2 != 0
131
+ ):
132
+ lines[index] = replace_all(lines[index], r"\\`|(`)", escape_all_backquote)
133
+
134
+ return "\n".join(lines)
135
+
136
+
137
+ def escape(text, flag=0, italic=True):
138
+ """
139
+ Convert Markdown to Telegram Markdown format.
140
+
141
+ Args:
142
+ text (str): Input Markdown text
143
+ flag (int): Processing flag
144
+ italic (bool): Whether to process italic formatting
145
+
146
+ Returns:
147
+ str: Telegram-formatted markdown
148
+ """
149
+ # In all other places characters
150
+ # _ * [ ] ( ) ~ ` > # + - = | { } . !
151
+ # must be escaped with the preceding character '\'.
152
+ text = re.sub(r"\\\[", "@->@", text)
153
+ text = re.sub(r"\\]", "@<-@", text)
154
+ text = re.sub(r"\\\(", "@-->@", text)
155
+ text = re.sub(r"\\\)", "@<--@", text)
156
+ if flag:
157
+ text = re.sub(r"\\\\", "@@@", text)
158
+ text = re.sub(r"\\`", "@<@", text)
159
+ text = re.sub(r"\\", r"\\\\", text)
160
+ if flag:
161
+ text = re.sub(r"@{3}", r"\\\\", text)
162
+ # _italic_
163
+ if italic:
164
+ text = re.sub(r"_(.*?)_", "@@@\\1@@@", text)
165
+ text = re.sub(r"_", r"\_", text)
166
+ text = re.sub(r"@{3}(.*?)@{3}", "_\\1_", text)
167
+ else:
168
+ text = re.sub(r"_", r"\_", text)
169
+
170
+ text = re.sub(r"\*{2}(.*?)\*{2}", "@@@\\1@@@", text)
171
+ text = re.sub(r"\n{1,2}\*\s", "\n\n• ", text)
172
+ text = re.sub(r"\*", r"\*", text)
173
+ text = re.sub(r"@{3}(.*?)@{3}", "*\\1*", text)
174
+ text = re.sub(r"!?\[(.*?)]\((.*?)\)", "@@@\\1@@@^^^\\2^^^", text)
175
+ text = re.sub(r"\[", r"\[", text)
176
+ text = re.sub(r"]", r"\]", text)
177
+ text = re.sub(r"\(", r"\(", text)
178
+ text = re.sub(r"\)", r"\)", text)
179
+ text = re.sub(r"@->@", r"\[", text)
180
+ text = re.sub(r"@<-@", r"\]", text)
181
+ text = re.sub(r"@-->@", r"\(", text)
182
+ text = re.sub(r"@<--@", r"\)", text)
183
+ text = re.sub(r"@{3}(.*?)@{3}\^{3}(.*?)\^{3}", "[\\1](\\2)", text)
184
+
185
+ # ~strikethrough~
186
+ text = re.sub(r"~{2}(.*?)~{2}", "@@@\\1@@@", text)
187
+ text = re.sub(r"~", r"\~", text)
188
+ text = re.sub(r"@{3}(.*?)@{3}", "~\\1~", text)
189
+
190
+ text = re.sub(r"\n>\s", "\n@@@ ", text)
191
+ text = re.sub(r">", r"\>", text)
192
+ text = re.sub(r"@{3}", ">", text)
193
+
194
+ text = replace_all(text, r"(^#+\s.+?\n+)|```[\D\d\s]+?```", escapeshape)
195
+ text = re.sub(r"#", r"\#", text)
196
+ text = replace_all(text, r"(\+)|\n[\s]*-\s|```[\D\d\s]+?```|`[\D\d\s]*?`", escapeplus)
197
+
198
+ # Numbered lists
199
+ text = re.sub(r"\n{1,2}(\s*\d{1,2}\.\s)", "\n\n\\1", text)
200
+
201
+ # Replace - outside code blocks
202
+ text = replace_all(text, r"```[\D\d\s]+?```|(-)", escapeminus2)
203
+ text = re.sub(r"-", "@<+@", text)
204
+ text = re.sub(r"@\+>@", "-", text)
205
+
206
+ text = re.sub(r"\n{1,2}(\s*)-\s", "\n\n\\1• ", text)
207
+ text = re.sub(r"@<+@", r"\-", text)
208
+ text = replace_all(text, r"(-)|\n[\s]*-\s|```[\D\d\s]+?```|`[\D\d\s]*?`", escapeminus)
209
+ text = re.sub(r"```([\D\d\s]+?)```", "@@@\\1@@@", text)
210
+ # Replace backticks in code blocks
211
+ text = replace_all(text, r"\@\@\@[\s\d\D]+?\@\@\@|(`)", escapebackquoteincode)
212
+ text = re.sub(r"`", r"\`", text)
213
+ text = re.sub(r"@<@", r"\`", text)
214
+ text = re.sub(r"@->@", "`", text)
215
+ text = re.sub(r"\s`\\`\s", r" `\\\\\` ", text)
216
+
217
+ text = replace_all(text, r"(``)", escapebackquote)
218
+ text = re.sub(r"@{3}([\D\d\s]+?)@{3}", "```\\1```", text)
219
+ text = re.sub(r"=", r"\=", text)
220
+ text = re.sub(r"\|", r"\|", text)
221
+ # text = re.sub(r"\@\!\@", '||', text)
222
+ text = re.sub(r"{", r"\{", text)
223
+ text = re.sub(r"}", r"\}", text)
224
+ text = re.sub(r"\.", r"\.", text)
225
+ text = re.sub(r"!", r"\!", text)
226
+ text = find_lines_with_char(text, "`", 5)
227
+ text = replace_all(text, r"(\n+\x20*```[\D\d\s]+?```\n+)", dedent_space)
228
+ return text
229
+
230
+
231
+ test_text = r"""
232
+ # title
233
+
234
+ ### `probs.scatter_(1, ind`
235
+
236
+ **bold**
237
+ ```
238
+ # comment
239
+ print(qwer) # ferfe
240
+ ni1
241
+ ```
242
+ # bn
243
+
244
+
245
+ # b
246
+
247
+ # Header
248
+ ## Subheader
249
+
250
+ [1.0.0](http://version.com)
251
+ ![1.0.0](http://version.com)
252
+
253
+ - item 1 -
254
+ - item 1 -
255
+ - item 1 -
256
+ * item 2 #
257
+ * item 3 ~
258
+
259
+ 1. item 1
260
+ 2. item 2
261
+
262
+ 1. item 1
263
+ ```python
264
+
265
+ # comment
266
+ print("1.1\n")_
267
+ \subsubsection{1.1}
268
+ - item 1 -
269
+ ```
270
+ 2. item 2
271
+
272
+ sudo apt install package # Install command
273
+
274
+ \subsubsection{1.1}
275
+
276
+ And simple text `with-dashes` `with+plus` + some - **symbols**.
277
+
278
+ ```
279
+ print("Hello, World!") -
280
+ app.listen(PORT, () => {
281
+ console.log(`Server is running on http://localhost:${PORT}`);
282
+ });
283
+ ```
284
+
285
+ Cxy = abs (Pxy)**2/ (Pxx*Pyy)
286
+
287
+ `a`a-b-c`n`
288
+ \[ E[X^4] = \int_{-\infty}^{\infty} x^4 f(x) dx \]
289
+
290
+ `-a----++++`++a-b-c`-n-`
291
+ `[^``]*`a``b-c``d``
292
+ # pattern = r"`[^`]*`-([^`-]*)"``
293
+ w`-a----`ccccc`-n-`bbbb``a
294
+
295
+ 1. Open VSCode terminal: Go to `View` > `Terminal` or use `Ctrl+``
296
+
297
+ How to write: `line.strip().startswith("```")`?
298
+
299
+ `View` > `Terminal`
300
+
301
+ Escape example: `\``
302
+
303
+ - `Path.open()` method opens the `README.md` file with UTF-8 encoding.
304
+
305
+ 3. `(`
306
+
307
+ 3. Parentheses example: `(`
308
+
309
+ According to Euler's totient function, for \( n = p_1^{k_1} \times p_2^{k_2} \times \cdots \times p_r^{k_r} \) (where \( p_1, p_2, \ldots, p_r \) are distinct primes):
310
+
311
+ \[ \varphi(n) = n \left(1 - \frac{1}{p_1}\right) \left(1 - \frac{1}{p_2}\right) \cdots \left(1 - \frac{1}{p_r}\right) \]
312
+
313
+ Therefore:
314
+
315
+ \[ \varphi(35) = 35 \left(1 - \frac{1}{5}\right) \left(1 - \frac{1}{7}\right) \]
316
+
317
+ Calculating step by step:
318
+
319
+ \[ \varphi(35) = 35 \left(\frac{4}{5}\right) \left(\frac{6}{7}\right) \]
320
+
321
+ \[ \varphi(35) = 35 \times \frac{24}{35} \]
322
+
323
+ \[ \varphi(35) = 24 \]
324
+
325
+ To calculate acceleration \( a \), use the formula:
326
+
327
+ \[ a = \frac{\Delta v}{\Delta t} \]
328
+
329
+ Where:
330
+ - \(\Delta v\) is the change in velocity
331
+ - \(\Delta t\) is the change in time
332
+
333
+ Given:
334
+ - Initial velocity \( v_0 = 0 \) m/s
335
+ - Final velocity \( v = 27.8 \) m/s
336
+ - Time \( \Delta t = 3.85 \) s
337
+
338
+ Substituting values:
339
+
340
+ \[ a = \frac{27.8 \, \text{m/s} - 0 \, \text{m/s}}{3.85 \, \text{s}} \]
341
+
342
+ Calculating:
343
+
344
+ \[ a = \frac{27.8}{3.85} \approx 7.22 \, \text{m/s}^2 \]
345
+
346
+ Therefore, the car's acceleration is approximately 7.22 m/s².
347
+
348
+ Thus, Euler's totient function \( \varphi(35) \) equals 24.
349
+ """
350
+
351
+ if __name__ == "__main__":
352
+ test_text_escaped = escape(test_text)
353
+ print(test_text_escaped)
jims_telegram/py.typed ADDED
File without changes
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: jims-telegram
3
+ Version: 0.1.0.dev4
4
+ Summary: Telegram interface for JIMS applications
5
+ Author-email: Andrey Tatarinov <a@tatarinov.co>, Timur Sheydaev <tsheyd@epoch8.co>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: aiogram>=3.18
8
+ Requires-Dist: aiohttp>=3.11.16
9
+ Requires-Dist: click>=8.0
10
+ Requires-Dist: jims-core
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: pydantic-settings>=2.8.1
13
+ Requires-Dist: pydantic>=2.4.1
@@ -0,0 +1,8 @@
1
+ jims_telegram/__init__.py,sha256=0wEcTNYCGnmVcc9n8wfID7vtBfYhmJryrYx-8Sg1qc4,9970
2
+ jims_telegram/main.py,sha256=inu_Nh6W3OxDvlPZbYZZRK7bxHog3Dg_akKfiteKVrQ,2059
3
+ jims_telegram/md2tgmd.py,sha256=xermMXAr8tgbsFp7itxSPPJ0NfEe5A66fwoBxrO-__o,9461
4
+ jims_telegram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ jims_telegram-0.1.0.dev4.dist-info/METADATA,sha256=m4hW97HNPY7glFnRR3aPz3cIqEwE5YHVNBwP55yR3DU,431
6
+ jims_telegram-0.1.0.dev4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ jims_telegram-0.1.0.dev4.dist-info/entry_points.txt,sha256=eiEkp0hbAskEaY7YfCcWDXMs9NM_TdCg88UMA6pVx5Y,58
8
+ jims_telegram-0.1.0.dev4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jims-telegram = jims_telegram.main:main