yee88 0.1.0__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.
Files changed (103) hide show
  1. takopi/__init__.py +1 -0
  2. takopi/api.py +116 -0
  3. takopi/backends.py +25 -0
  4. takopi/backends_helpers.py +14 -0
  5. takopi/cli/__init__.py +228 -0
  6. takopi/cli/config.py +320 -0
  7. takopi/cli/doctor.py +173 -0
  8. takopi/cli/init.py +113 -0
  9. takopi/cli/onboarding_cmd.py +126 -0
  10. takopi/cli/plugins.py +196 -0
  11. takopi/cli/run.py +419 -0
  12. takopi/cli/topic.py +355 -0
  13. takopi/commands.py +134 -0
  14. takopi/config.py +142 -0
  15. takopi/config_migrations.py +124 -0
  16. takopi/config_watch.py +146 -0
  17. takopi/context.py +9 -0
  18. takopi/directives.py +146 -0
  19. takopi/engines.py +53 -0
  20. takopi/events.py +170 -0
  21. takopi/ids.py +17 -0
  22. takopi/lockfile.py +158 -0
  23. takopi/logging.py +283 -0
  24. takopi/markdown.py +298 -0
  25. takopi/model.py +77 -0
  26. takopi/plugins.py +312 -0
  27. takopi/presenter.py +25 -0
  28. takopi/progress.py +99 -0
  29. takopi/router.py +113 -0
  30. takopi/runner.py +712 -0
  31. takopi/runner_bridge.py +619 -0
  32. takopi/runners/__init__.py +1 -0
  33. takopi/runners/claude.py +483 -0
  34. takopi/runners/codex.py +656 -0
  35. takopi/runners/mock.py +221 -0
  36. takopi/runners/opencode.py +505 -0
  37. takopi/runners/pi.py +523 -0
  38. takopi/runners/run_options.py +39 -0
  39. takopi/runners/tool_actions.py +90 -0
  40. takopi/runtime_loader.py +207 -0
  41. takopi/scheduler.py +159 -0
  42. takopi/schemas/__init__.py +1 -0
  43. takopi/schemas/claude.py +238 -0
  44. takopi/schemas/codex.py +169 -0
  45. takopi/schemas/opencode.py +51 -0
  46. takopi/schemas/pi.py +117 -0
  47. takopi/settings.py +360 -0
  48. takopi/telegram/__init__.py +20 -0
  49. takopi/telegram/api_models.py +37 -0
  50. takopi/telegram/api_schemas.py +152 -0
  51. takopi/telegram/backend.py +163 -0
  52. takopi/telegram/bridge.py +425 -0
  53. takopi/telegram/chat_prefs.py +242 -0
  54. takopi/telegram/chat_sessions.py +112 -0
  55. takopi/telegram/client.py +409 -0
  56. takopi/telegram/client_api.py +539 -0
  57. takopi/telegram/commands/__init__.py +12 -0
  58. takopi/telegram/commands/agent.py +196 -0
  59. takopi/telegram/commands/cancel.py +116 -0
  60. takopi/telegram/commands/dispatch.py +111 -0
  61. takopi/telegram/commands/executor.py +449 -0
  62. takopi/telegram/commands/file_transfer.py +586 -0
  63. takopi/telegram/commands/handlers.py +45 -0
  64. takopi/telegram/commands/media.py +143 -0
  65. takopi/telegram/commands/menu.py +139 -0
  66. takopi/telegram/commands/model.py +215 -0
  67. takopi/telegram/commands/overrides.py +159 -0
  68. takopi/telegram/commands/parse.py +30 -0
  69. takopi/telegram/commands/plan.py +16 -0
  70. takopi/telegram/commands/reasoning.py +234 -0
  71. takopi/telegram/commands/reply.py +23 -0
  72. takopi/telegram/commands/topics.py +332 -0
  73. takopi/telegram/commands/trigger.py +143 -0
  74. takopi/telegram/context.py +140 -0
  75. takopi/telegram/engine_defaults.py +86 -0
  76. takopi/telegram/engine_overrides.py +105 -0
  77. takopi/telegram/files.py +178 -0
  78. takopi/telegram/loop.py +1822 -0
  79. takopi/telegram/onboarding.py +1088 -0
  80. takopi/telegram/outbox.py +177 -0
  81. takopi/telegram/parsing.py +239 -0
  82. takopi/telegram/render.py +198 -0
  83. takopi/telegram/state_store.py +88 -0
  84. takopi/telegram/topic_state.py +334 -0
  85. takopi/telegram/topics.py +256 -0
  86. takopi/telegram/trigger_mode.py +68 -0
  87. takopi/telegram/types.py +63 -0
  88. takopi/telegram/voice.py +110 -0
  89. takopi/transport.py +53 -0
  90. takopi/transport_runtime.py +323 -0
  91. takopi/transports.py +76 -0
  92. takopi/utils/__init__.py +1 -0
  93. takopi/utils/git.py +87 -0
  94. takopi/utils/json_state.py +21 -0
  95. takopi/utils/paths.py +47 -0
  96. takopi/utils/streams.py +44 -0
  97. takopi/utils/subprocess.py +86 -0
  98. takopi/worktrees.py +135 -0
  99. yee88-0.1.0.dist-info/METADATA +116 -0
  100. yee88-0.1.0.dist-info/RECORD +103 -0
  101. yee88-0.1.0.dist-info/WHEEL +4 -0
  102. yee88-0.1.0.dist-info/entry_points.txt +11 -0
  103. yee88-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import msgspec
6
+
7
+ from ..logging import get_logger
8
+ from ..model import ResumeToken
9
+ from .state_store import JsonStateStore
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ STATE_VERSION = 1
14
+ STATE_FILENAME = "telegram_chat_sessions_state.json"
15
+
16
+
17
+ class _SessionState(msgspec.Struct, forbid_unknown_fields=False):
18
+ resume: str
19
+
20
+
21
+ class _ChatState(msgspec.Struct, forbid_unknown_fields=False):
22
+ sessions: dict[str, _SessionState] = msgspec.field(default_factory=dict)
23
+
24
+
25
+ class _ChatSessionsState(msgspec.Struct, forbid_unknown_fields=False):
26
+ version: int
27
+ cwd: str | None = None
28
+ chats: dict[str, _ChatState] = msgspec.field(default_factory=dict)
29
+
30
+
31
+ def resolve_sessions_path(config_path: Path) -> Path:
32
+ return config_path.with_name(STATE_FILENAME)
33
+
34
+
35
+ def _chat_key(chat_id: int, owner_id: int | None) -> str:
36
+ owner = "chat" if owner_id is None else str(owner_id)
37
+ return f"{chat_id}:{owner}"
38
+
39
+
40
+ def _new_state() -> _ChatSessionsState:
41
+ return _ChatSessionsState(version=STATE_VERSION, chats={})
42
+
43
+
44
+ class ChatSessionStore(JsonStateStore[_ChatSessionsState]):
45
+ def __init__(self, path: Path) -> None:
46
+ super().__init__(
47
+ path,
48
+ version=STATE_VERSION,
49
+ state_type=_ChatSessionsState,
50
+ state_factory=_new_state,
51
+ log_prefix="telegram.chat_sessions",
52
+ logger=logger,
53
+ )
54
+
55
+ async def get_session_resume(
56
+ self, chat_id: int, owner_id: int | None, engine: str
57
+ ) -> ResumeToken | None:
58
+ async with self._lock:
59
+ self._reload_locked_if_needed()
60
+ chat = self._get_chat_locked(chat_id, owner_id)
61
+ if chat is None:
62
+ return None
63
+ entry = chat.sessions.get(engine)
64
+ if entry is None or not entry.resume:
65
+ return None
66
+ return ResumeToken(engine=engine, value=entry.resume)
67
+
68
+ async def sync_startup_cwd(self, cwd: Path) -> bool:
69
+ normalized = str(cwd.expanduser().resolve())
70
+ async with self._lock:
71
+ self._reload_locked_if_needed()
72
+ previous = self._state.cwd
73
+ cleared = False
74
+ if previous is not None and previous != normalized:
75
+ self._state.chats = {}
76
+ cleared = True
77
+ if previous != normalized:
78
+ self._state.cwd = normalized
79
+ self._save_locked()
80
+ return cleared
81
+
82
+ async def set_session_resume(
83
+ self, chat_id: int, owner_id: int | None, token: ResumeToken
84
+ ) -> None:
85
+ async with self._lock:
86
+ self._reload_locked_if_needed()
87
+ if self._state.cwd is None:
88
+ self._state.cwd = str(Path.cwd().expanduser().resolve())
89
+ chat = self._ensure_chat_locked(chat_id, owner_id)
90
+ chat.sessions[token.engine] = _SessionState(resume=token.value)
91
+ self._save_locked()
92
+
93
+ async def clear_sessions(self, chat_id: int, owner_id: int | None) -> None:
94
+ async with self._lock:
95
+ self._reload_locked_if_needed()
96
+ chat = self._get_chat_locked(chat_id, owner_id)
97
+ if chat is None:
98
+ return
99
+ chat.sessions = {}
100
+ self._save_locked()
101
+
102
+ def _get_chat_locked(self, chat_id: int, owner_id: int | None) -> _ChatState | None:
103
+ return self._state.chats.get(_chat_key(chat_id, owner_id))
104
+
105
+ def _ensure_chat_locked(self, chat_id: int, owner_id: int | None) -> _ChatState:
106
+ key = _chat_key(chat_id, owner_id)
107
+ entry = self._state.chats.get(key)
108
+ if entry is not None:
109
+ return entry
110
+ entry = _ChatState()
111
+ self._state.chats[key] = entry
112
+ return entry
@@ -0,0 +1,409 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import time
5
+ from typing import Any
6
+ from collections.abc import Awaitable, Callable, Hashable
7
+
8
+ import anyio
9
+ import httpx
10
+
11
+ from ..logging import get_logger
12
+ from .api_models import Chat, ChatMember, File, ForumTopic, Message, Update, User
13
+ from .client_api import BotClient, HttpBotClient, TelegramRetryAfter
14
+ from .outbox import (
15
+ DELETE_PRIORITY,
16
+ EDIT_PRIORITY,
17
+ SEND_PRIORITY,
18
+ OutboxOp,
19
+ TelegramOutbox,
20
+ )
21
+ from .parsing import parse_incoming_update, poll_incoming
22
+
23
+ logger = get_logger(__name__)
24
+
25
+ __all__ = [
26
+ "BotClient",
27
+ "TelegramClient",
28
+ "TelegramRetryAfter",
29
+ "is_group_chat_id",
30
+ "parse_incoming_update",
31
+ "poll_incoming",
32
+ ]
33
+
34
+
35
+ def is_group_chat_id(chat_id: int) -> bool:
36
+ return chat_id < 0
37
+
38
+
39
+ class TelegramClient:
40
+ def __init__(
41
+ self,
42
+ token: str | None = None,
43
+ *,
44
+ client: BotClient | None = None,
45
+ timeout_s: float = 120,
46
+ http_client: httpx.AsyncClient | None = None,
47
+ clock: Callable[[], float] = time.monotonic,
48
+ sleep: Callable[[float], Awaitable[None]] = anyio.sleep,
49
+ private_chat_rps: float = 1.0,
50
+ group_chat_rps: float = 20.0 / 60.0,
51
+ ) -> None:
52
+ if client is not None:
53
+ if token is not None or http_client is not None:
54
+ raise ValueError("Provide either token or client, not both.")
55
+ self._client = client
56
+ else:
57
+ if token is None or not token:
58
+ raise ValueError("Telegram token is empty")
59
+ self._client = HttpBotClient(
60
+ token,
61
+ timeout_s=timeout_s,
62
+ http_client=http_client,
63
+ )
64
+ self._clock = clock
65
+ self._sleep = sleep
66
+ self._private_interval = (
67
+ 0.0 if private_chat_rps <= 0 else 1.0 / private_chat_rps
68
+ )
69
+ self._group_interval = 0.0 if group_chat_rps <= 0 else 1.0 / group_chat_rps
70
+ self._outbox = TelegramOutbox(
71
+ interval_for_chat=self.interval_for_chat,
72
+ clock=clock,
73
+ sleep=sleep,
74
+ on_error=self.log_request_error,
75
+ on_outbox_error=self.log_outbox_failure,
76
+ )
77
+ self._seq = itertools.count()
78
+
79
+ def interval_for_chat(self, chat_id: int | None) -> float:
80
+ if chat_id is None:
81
+ return self._private_interval
82
+ if is_group_chat_id(chat_id):
83
+ return self._group_interval
84
+ return self._private_interval
85
+
86
+ def log_request_error(self, request: OutboxOp, exc: Exception) -> None:
87
+ logger.error(
88
+ "telegram.outbox.request_failed",
89
+ method=request.label,
90
+ error=str(exc),
91
+ error_type=exc.__class__.__name__,
92
+ )
93
+
94
+ def log_outbox_failure(self, exc: Exception) -> None:
95
+ logger.error(
96
+ "telegram.outbox.failed",
97
+ error=str(exc),
98
+ error_type=exc.__class__.__name__,
99
+ )
100
+
101
+ async def drop_pending_edits(self, *, chat_id: int, message_id: int) -> None:
102
+ await self._outbox.drop_pending(key=("edit", chat_id, message_id))
103
+
104
+ def unique_key(self, prefix: str) -> tuple[str, int]:
105
+ return (prefix, next(self._seq))
106
+
107
+ async def enqueue_op(
108
+ self,
109
+ *,
110
+ key: Hashable,
111
+ label: str,
112
+ execute: Callable[[], Awaitable[Any]],
113
+ priority: int,
114
+ chat_id: int | None,
115
+ wait: bool = True,
116
+ ) -> Any:
117
+ request = OutboxOp(
118
+ execute=execute,
119
+ priority=priority,
120
+ queued_at=self._clock(),
121
+ chat_id=chat_id,
122
+ label=label,
123
+ )
124
+ return await self._outbox.enqueue(key=key, op=request, wait=wait)
125
+
126
+ async def close(self) -> None:
127
+ await self._outbox.close()
128
+ await self._client.close()
129
+
130
+ async def _call_with_retry_after(
131
+ self,
132
+ fn: Callable[[], Awaitable[Any]],
133
+ ) -> Any:
134
+ while True:
135
+ try:
136
+ return await fn()
137
+ except TelegramRetryAfter as exc:
138
+ await self._sleep(exc.retry_after)
139
+
140
+ async def get_updates(
141
+ self,
142
+ offset: int | None,
143
+ timeout_s: int = 50,
144
+ allowed_updates: list[str] | None = None,
145
+ ) -> list[Update] | None:
146
+ async def execute() -> list[Update] | None:
147
+ return await self._client.get_updates(
148
+ offset=offset,
149
+ timeout_s=timeout_s,
150
+ allowed_updates=allowed_updates,
151
+ )
152
+
153
+ return await self._call_with_retry_after(execute)
154
+
155
+ async def get_file(self, file_id: str) -> File | None:
156
+ async def execute() -> File | None:
157
+ return await self._client.get_file(file_id)
158
+
159
+ return await self._call_with_retry_after(execute)
160
+
161
+ async def download_file(self, file_path: str) -> bytes | None:
162
+ async def execute() -> bytes | None:
163
+ return await self._client.download_file(file_path)
164
+
165
+ return await self._call_with_retry_after(execute)
166
+
167
+ async def send_message(
168
+ self,
169
+ chat_id: int,
170
+ text: str,
171
+ reply_to_message_id: int | None = None,
172
+ disable_notification: bool | None = False,
173
+ message_thread_id: int | None = None,
174
+ entities: list[dict] | None = None,
175
+ parse_mode: str | None = None,
176
+ reply_markup: dict[str, Any] | None = None,
177
+ *,
178
+ replace_message_id: int | None = None,
179
+ ) -> Message | None:
180
+ async def execute() -> Message | None:
181
+ return await self._client.send_message(
182
+ chat_id=chat_id,
183
+ text=text,
184
+ reply_to_message_id=reply_to_message_id,
185
+ disable_notification=disable_notification,
186
+ message_thread_id=message_thread_id,
187
+ entities=entities,
188
+ parse_mode=parse_mode,
189
+ reply_markup=reply_markup,
190
+ replace_message_id=replace_message_id,
191
+ )
192
+
193
+ if replace_message_id is not None:
194
+ await self._outbox.drop_pending(key=("edit", chat_id, replace_message_id))
195
+ result = await self.enqueue_op(
196
+ key=(
197
+ ("send", chat_id, replace_message_id)
198
+ if replace_message_id is not None
199
+ else self.unique_key("send")
200
+ ),
201
+ label="send_message",
202
+ execute=execute,
203
+ priority=SEND_PRIORITY,
204
+ chat_id=chat_id,
205
+ )
206
+ if replace_message_id is not None and result is not None:
207
+ await self.delete_message(chat_id=chat_id, message_id=replace_message_id)
208
+ return result
209
+
210
+ async def send_document(
211
+ self,
212
+ chat_id: int,
213
+ filename: str,
214
+ content: bytes,
215
+ reply_to_message_id: int | None = None,
216
+ message_thread_id: int | None = None,
217
+ disable_notification: bool | None = False,
218
+ caption: str | None = None,
219
+ ) -> Message | None:
220
+ async def execute() -> Message | None:
221
+ return await self._client.send_document(
222
+ chat_id=chat_id,
223
+ filename=filename,
224
+ content=content,
225
+ reply_to_message_id=reply_to_message_id,
226
+ message_thread_id=message_thread_id,
227
+ disable_notification=disable_notification,
228
+ caption=caption,
229
+ )
230
+
231
+ return await self.enqueue_op(
232
+ key=self.unique_key("send_document"),
233
+ label="send_document",
234
+ execute=execute,
235
+ priority=SEND_PRIORITY,
236
+ chat_id=chat_id,
237
+ )
238
+
239
+ async def edit_message_text(
240
+ self,
241
+ chat_id: int,
242
+ message_id: int,
243
+ text: str,
244
+ entities: list[dict] | None = None,
245
+ parse_mode: str | None = None,
246
+ reply_markup: dict[str, Any] | None = None,
247
+ *,
248
+ wait: bool = True,
249
+ ) -> Message | None:
250
+ async def execute() -> Message | None:
251
+ return await self._client.edit_message_text(
252
+ chat_id=chat_id,
253
+ message_id=message_id,
254
+ text=text,
255
+ entities=entities,
256
+ parse_mode=parse_mode,
257
+ reply_markup=reply_markup,
258
+ wait=wait,
259
+ )
260
+
261
+ return await self.enqueue_op(
262
+ key=("edit", chat_id, message_id),
263
+ label="edit_message_text",
264
+ execute=execute,
265
+ priority=EDIT_PRIORITY,
266
+ chat_id=chat_id,
267
+ wait=wait,
268
+ )
269
+
270
+ async def delete_message(
271
+ self,
272
+ chat_id: int,
273
+ message_id: int,
274
+ ) -> bool:
275
+ await self.drop_pending_edits(chat_id=chat_id, message_id=message_id)
276
+
277
+ async def execute() -> bool:
278
+ return await self._client.delete_message(
279
+ chat_id=chat_id,
280
+ message_id=message_id,
281
+ )
282
+
283
+ return bool(
284
+ await self.enqueue_op(
285
+ key=("delete", chat_id, message_id),
286
+ label="delete_message",
287
+ execute=execute,
288
+ priority=DELETE_PRIORITY,
289
+ chat_id=chat_id,
290
+ )
291
+ )
292
+
293
+ async def set_my_commands(
294
+ self,
295
+ commands: list[dict[str, Any]],
296
+ *,
297
+ scope: dict[str, Any] | None = None,
298
+ language_code: str | None = None,
299
+ ) -> bool:
300
+ async def execute() -> bool:
301
+ return await self._client.set_my_commands(
302
+ commands,
303
+ scope=scope,
304
+ language_code=language_code,
305
+ )
306
+
307
+ return bool(
308
+ await self.enqueue_op(
309
+ key=self.unique_key("set_my_commands"),
310
+ label="set_my_commands",
311
+ execute=execute,
312
+ priority=SEND_PRIORITY,
313
+ chat_id=None,
314
+ )
315
+ )
316
+
317
+ async def get_me(self) -> User | None:
318
+ async def execute() -> User | None:
319
+ return await self._client.get_me()
320
+
321
+ return await self.enqueue_op(
322
+ key=self.unique_key("get_me"),
323
+ label="get_me",
324
+ execute=execute,
325
+ priority=SEND_PRIORITY,
326
+ chat_id=None,
327
+ )
328
+
329
+ async def answer_callback_query(
330
+ self,
331
+ callback_query_id: str,
332
+ text: str | None = None,
333
+ show_alert: bool | None = None,
334
+ ) -> bool:
335
+ async def execute() -> bool:
336
+ return await self._client.answer_callback_query(
337
+ callback_query_id=callback_query_id,
338
+ text=text,
339
+ show_alert=show_alert,
340
+ )
341
+
342
+ return bool(
343
+ await self.enqueue_op(
344
+ key=self.unique_key("answer_callback_query"),
345
+ label="answer_callback_query",
346
+ execute=execute,
347
+ priority=SEND_PRIORITY,
348
+ chat_id=None,
349
+ )
350
+ )
351
+
352
+ async def get_chat(self, chat_id: int) -> Chat | None:
353
+ async def execute() -> Chat | None:
354
+ return await self._client.get_chat(chat_id)
355
+
356
+ return await self.enqueue_op(
357
+ key=self.unique_key("get_chat"),
358
+ label="get_chat",
359
+ execute=execute,
360
+ priority=SEND_PRIORITY,
361
+ chat_id=chat_id,
362
+ )
363
+
364
+ async def get_chat_member(self, chat_id: int, user_id: int) -> ChatMember | None:
365
+ async def execute() -> ChatMember | None:
366
+ return await self._client.get_chat_member(chat_id, user_id)
367
+
368
+ return await self.enqueue_op(
369
+ key=self.unique_key("get_chat_member"),
370
+ label="get_chat_member",
371
+ execute=execute,
372
+ priority=SEND_PRIORITY,
373
+ chat_id=chat_id,
374
+ )
375
+
376
+ async def create_forum_topic(self, chat_id: int, name: str) -> ForumTopic | None:
377
+ async def execute() -> ForumTopic | None:
378
+ return await self._client.create_forum_topic(chat_id, name)
379
+
380
+ return await self.enqueue_op(
381
+ key=self.unique_key("create_forum_topic"),
382
+ label="create_forum_topic",
383
+ execute=execute,
384
+ priority=SEND_PRIORITY,
385
+ chat_id=chat_id,
386
+ )
387
+
388
+ async def edit_forum_topic(
389
+ self,
390
+ chat_id: int,
391
+ message_thread_id: int,
392
+ name: str,
393
+ ) -> bool:
394
+ async def execute() -> bool:
395
+ return await self._client.edit_forum_topic(
396
+ chat_id,
397
+ message_thread_id,
398
+ name,
399
+ )
400
+
401
+ return bool(
402
+ await self.enqueue_op(
403
+ key=self.unique_key("edit_forum_topic"),
404
+ label="edit_forum_topic",
405
+ execute=execute,
406
+ priority=SEND_PRIORITY,
407
+ chat_id=chat_id,
408
+ )
409
+ )