devcopilot 0.2.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 (189) hide show
  1. api/__init__.py +17 -0
  2. api/admin_config.py +1303 -0
  3. api/admin_routes.py +287 -0
  4. api/admin_static/admin.css +459 -0
  5. api/admin_static/admin.js +497 -0
  6. api/admin_static/index.html +77 -0
  7. api/admin_urls.py +34 -0
  8. api/app.py +194 -0
  9. api/command_utils.py +164 -0
  10. api/dependencies.py +144 -0
  11. api/detection.py +152 -0
  12. api/gateway_model_ids.py +54 -0
  13. api/model_catalog.py +133 -0
  14. api/model_router.py +125 -0
  15. api/models/__init__.py +45 -0
  16. api/models/anthropic.py +234 -0
  17. api/models/openai_responses.py +28 -0
  18. api/models/responses.py +60 -0
  19. api/optimization_handlers.py +154 -0
  20. api/request_pipeline.py +424 -0
  21. api/routes.py +156 -0
  22. api/runtime.py +334 -0
  23. api/validation_log.py +48 -0
  24. api/web_server_tools.py +22 -0
  25. api/web_tools/__init__.py +17 -0
  26. api/web_tools/constants.py +15 -0
  27. api/web_tools/egress.py +99 -0
  28. api/web_tools/outbound.py +278 -0
  29. api/web_tools/parsers.py +104 -0
  30. api/web_tools/request.py +87 -0
  31. api/web_tools/streaming.py +206 -0
  32. cli/__init__.py +5 -0
  33. cli/claude_env.py +12 -0
  34. cli/entrypoints.py +166 -0
  35. cli/env.example +209 -0
  36. cli/launchers/__init__.py +1 -0
  37. cli/launchers/claude.py +84 -0
  38. cli/launchers/codex.py +204 -0
  39. cli/launchers/codex_model_catalog.py +186 -0
  40. cli/launchers/common.py +93 -0
  41. cli/managed/__init__.py +6 -0
  42. cli/managed/claude.py +215 -0
  43. cli/managed/manager.py +157 -0
  44. cli/managed/session.py +260 -0
  45. cli/process_registry.py +78 -0
  46. config/__init__.py +5 -0
  47. config/constants.py +13 -0
  48. config/logging_config.py +159 -0
  49. config/nim.py +118 -0
  50. config/paths.py +91 -0
  51. config/provider_catalog.py +259 -0
  52. config/provider_ids.py +7 -0
  53. config/settings.py +538 -0
  54. core/__init__.py +1 -0
  55. core/anthropic/__init__.py +46 -0
  56. core/anthropic/content.py +31 -0
  57. core/anthropic/conversion.py +587 -0
  58. core/anthropic/emitted_sse_tracker.py +346 -0
  59. core/anthropic/errors.py +70 -0
  60. core/anthropic/native_messages_request.py +280 -0
  61. core/anthropic/native_sse_block_policy.py +313 -0
  62. core/anthropic/provider_stream_error.py +34 -0
  63. core/anthropic/server_tool_sse.py +14 -0
  64. core/anthropic/sse.py +440 -0
  65. core/anthropic/stream_contracts.py +205 -0
  66. core/anthropic/stream_recovery.py +346 -0
  67. core/anthropic/stream_recovery_session.py +133 -0
  68. core/anthropic/thinking.py +140 -0
  69. core/anthropic/tokens.py +117 -0
  70. core/anthropic/tools.py +212 -0
  71. core/anthropic/utils.py +9 -0
  72. core/openai_responses/__init__.py +5 -0
  73. core/openai_responses/adapter.py +31 -0
  74. core/openai_responses/anthropic_sse.py +59 -0
  75. core/openai_responses/errors.py +22 -0
  76. core/openai_responses/events.py +19 -0
  77. core/openai_responses/ids.py +21 -0
  78. core/openai_responses/input.py +258 -0
  79. core/openai_responses/items.py +37 -0
  80. core/openai_responses/reasoning.py +52 -0
  81. core/openai_responses/stream.py +25 -0
  82. core/openai_responses/stream_state.py +654 -0
  83. core/openai_responses/tools.py +374 -0
  84. core/openai_responses/usage.py +37 -0
  85. core/rate_limit.py +60 -0
  86. core/trace.py +216 -0
  87. devcopilot-0.2.0.dist-info/METADATA +687 -0
  88. devcopilot-0.2.0.dist-info/RECORD +189 -0
  89. devcopilot-0.2.0.dist-info/WHEEL +4 -0
  90. devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
  91. devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
  92. messaging/__init__.py +26 -0
  93. messaging/cli_event_constants.py +67 -0
  94. messaging/command_context.py +66 -0
  95. messaging/command_dispatcher.py +37 -0
  96. messaging/commands.py +275 -0
  97. messaging/event_parser.py +181 -0
  98. messaging/limiter.py +300 -0
  99. messaging/models.py +36 -0
  100. messaging/node_event_pipeline.py +127 -0
  101. messaging/node_runner.py +342 -0
  102. messaging/platforms/__init__.py +15 -0
  103. messaging/platforms/base.py +228 -0
  104. messaging/platforms/discord.py +567 -0
  105. messaging/platforms/factory.py +103 -0
  106. messaging/platforms/outbox.py +144 -0
  107. messaging/platforms/telegram.py +688 -0
  108. messaging/platforms/voice_flow.py +295 -0
  109. messaging/rendering/__init__.py +3 -0
  110. messaging/rendering/discord_markdown.py +318 -0
  111. messaging/rendering/markdown_tables.py +49 -0
  112. messaging/rendering/profiles.py +55 -0
  113. messaging/rendering/telegram_markdown.py +327 -0
  114. messaging/safe_diagnostics.py +17 -0
  115. messaging/session.py +334 -0
  116. messaging/transcript.py +581 -0
  117. messaging/transcription.py +164 -0
  118. messaging/trees/__init__.py +15 -0
  119. messaging/trees/data.py +482 -0
  120. messaging/trees/manager.py +433 -0
  121. messaging/trees/processor.py +179 -0
  122. messaging/trees/repository.py +177 -0
  123. messaging/turn_intake.py +235 -0
  124. messaging/ui_updates.py +101 -0
  125. messaging/voice.py +76 -0
  126. messaging/workflow.py +200 -0
  127. providers/__init__.py +31 -0
  128. providers/base.py +152 -0
  129. providers/cerebras/__init__.py +7 -0
  130. providers/cerebras/client.py +31 -0
  131. providers/cerebras/request.py +55 -0
  132. providers/codestral/__init__.py +7 -0
  133. providers/codestral/client.py +34 -0
  134. providers/deepseek/__init__.py +11 -0
  135. providers/deepseek/client.py +51 -0
  136. providers/deepseek/request.py +475 -0
  137. providers/defaults.py +41 -0
  138. providers/error_mapping.py +309 -0
  139. providers/exceptions.py +113 -0
  140. providers/fireworks/__init__.py +5 -0
  141. providers/fireworks/client.py +45 -0
  142. providers/fireworks/request.py +48 -0
  143. providers/gemini/__init__.py +7 -0
  144. providers/gemini/client.py +49 -0
  145. providers/gemini/request.py +199 -0
  146. providers/groq/__init__.py +7 -0
  147. providers/groq/client.py +31 -0
  148. providers/groq/request.py +83 -0
  149. providers/kimi/__init__.py +10 -0
  150. providers/kimi/client.py +53 -0
  151. providers/kimi/request.py +42 -0
  152. providers/llamacpp/__init__.py +3 -0
  153. providers/llamacpp/client.py +16 -0
  154. providers/lmstudio/__init__.py +5 -0
  155. providers/lmstudio/client.py +16 -0
  156. providers/mistral/__init__.py +7 -0
  157. providers/mistral/client.py +31 -0
  158. providers/mistral/request.py +37 -0
  159. providers/model_listing.py +133 -0
  160. providers/nvidia_nim/__init__.py +7 -0
  161. providers/nvidia_nim/client.py +91 -0
  162. providers/nvidia_nim/request.py +430 -0
  163. providers/nvidia_nim/voice.py +95 -0
  164. providers/ollama/__init__.py +7 -0
  165. providers/ollama/client.py +39 -0
  166. providers/open_router/__init__.py +7 -0
  167. providers/open_router/client.py +124 -0
  168. providers/open_router/request.py +42 -0
  169. providers/opencode/__init__.py +11 -0
  170. providers/opencode/client.py +31 -0
  171. providers/opencode/request.py +35 -0
  172. providers/rate_limit.py +300 -0
  173. providers/registry.py +527 -0
  174. providers/transports/__init__.py +1 -0
  175. providers/transports/anthropic_messages/__init__.py +5 -0
  176. providers/transports/anthropic_messages/http.py +118 -0
  177. providers/transports/anthropic_messages/recovery.py +206 -0
  178. providers/transports/anthropic_messages/stream.py +295 -0
  179. providers/transports/anthropic_messages/transport.py +236 -0
  180. providers/transports/openai_chat/__init__.py +5 -0
  181. providers/transports/openai_chat/recovery.py +217 -0
  182. providers/transports/openai_chat/stream.py +384 -0
  183. providers/transports/openai_chat/tool_calls.py +293 -0
  184. providers/transports/openai_chat/transport.py +156 -0
  185. providers/wafer/__init__.py +10 -0
  186. providers/wafer/client.py +50 -0
  187. providers/zai/__init__.py +10 -0
  188. providers/zai/client.py +46 -0
  189. providers/zai/request.py +42 -0
@@ -0,0 +1,342 @@
1
+ """Run queued messaging nodes through a managed CLI session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Callable
7
+
8
+ from loguru import logger
9
+
10
+ from core.anthropic import format_user_error_preview, get_user_facing_error_message
11
+ from core.trace import trace_event
12
+
13
+ from .event_parser import parse_cli_event
14
+ from .node_event_pipeline import handle_session_info_event, process_parsed_cli_event
15
+ from .platforms.base import ManagedClaudeSessionManagerProtocol, MessagingPlatform
16
+ from .safe_diagnostics import format_exception_for_log
17
+ from .session import SessionStore
18
+ from .transcript import RenderCtx, TranscriptBuffer
19
+ from .trees import MessageNode, MessageState, MessageTree, TreeQueueManager
20
+ from .ui_updates import ThrottledTranscriptEditor
21
+
22
+
23
+ class MessagingNodeRunner:
24
+ """Owns the lifecycle of one queued messaging node."""
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ platform: MessagingPlatform,
30
+ cli_manager: ManagedClaudeSessionManagerProtocol,
31
+ session_store: SessionStore,
32
+ get_tree_queue: Callable[[], TreeQueueManager],
33
+ format_status: Callable[[str, str, str | None], str],
34
+ get_parse_mode: Callable[[], str | None],
35
+ get_render_ctx: Callable[[], RenderCtx],
36
+ get_limit_chars: Callable[[], int],
37
+ debug_platform_edits: bool = False,
38
+ debug_subagent_stack: bool = False,
39
+ log_raw_cli_diagnostics: bool = False,
40
+ log_messaging_error_details: bool = False,
41
+ ) -> None:
42
+ self.platform = platform
43
+ self.cli_manager = cli_manager
44
+ self.session_store = session_store
45
+ self._get_tree_queue = get_tree_queue
46
+ self._format_status = format_status
47
+ self._get_parse_mode = get_parse_mode
48
+ self._get_render_ctx = get_render_ctx
49
+ self._get_limit_chars = get_limit_chars
50
+ self._debug_platform_edits = debug_platform_edits
51
+ self._debug_subagent_stack = debug_subagent_stack
52
+ self._log_raw_cli_diagnostics = log_raw_cli_diagnostics
53
+ self._log_messaging_error_details = log_messaging_error_details
54
+
55
+ def _create_transcript_and_render_ctx(
56
+ self,
57
+ ) -> tuple[TranscriptBuffer, RenderCtx]:
58
+ """Create transcript buffer and render context for node processing."""
59
+ transcript = TranscriptBuffer(
60
+ show_tool_results=False,
61
+ debug_subagent_stack=self._debug_subagent_stack,
62
+ )
63
+ return transcript, self._get_render_ctx()
64
+
65
+ def _save_tree(self, tree: MessageTree | None) -> None:
66
+ """Persist tree state after runner-owned mutations."""
67
+ if tree:
68
+ self.session_store.save_tree(tree.root_id, tree.to_dict())
69
+
70
+ async def process_node(
71
+ self,
72
+ node_id: str,
73
+ node: MessageNode,
74
+ ) -> None:
75
+ """Core task processor for a single CLI interaction."""
76
+ incoming = node.incoming
77
+ status_msg_id = node.status_message_id
78
+ chat_id = incoming.chat_id
79
+
80
+ with logger.contextualize(node_id=node_id, chat_id=chat_id):
81
+ await self._process_node_impl(node_id, node, chat_id, status_msg_id)
82
+
83
+ async def _process_node_impl(
84
+ self,
85
+ node_id: str,
86
+ node: MessageNode,
87
+ chat_id: str,
88
+ status_msg_id: str,
89
+ ) -> None:
90
+ """Internal implementation of process_node with context bound."""
91
+ incoming = node.incoming
92
+
93
+ tree_queue = self._get_tree_queue()
94
+ tree = tree_queue.get_tree_for_node(node_id)
95
+ if tree:
96
+ await tree.update_state(node_id, MessageState.IN_PROGRESS)
97
+
98
+ transcript, render_ctx = self._create_transcript_and_render_ctx()
99
+
100
+ had_transcript_events = False
101
+ captured_session_id = None
102
+ temp_session_id = None
103
+ last_status: str | None = None
104
+
105
+ parent_session_id = None
106
+ platform_nm = getattr(self.platform, "name", "messaging")
107
+ if tree and node.parent_id:
108
+ parent_session_id = tree.get_parent_session_id(node_id)
109
+ if parent_session_id:
110
+ trace_event(
111
+ stage="claude_cli",
112
+ event="claude_cli.fork.from_parent_session",
113
+ source=platform_nm,
114
+ chat_id=chat_id,
115
+ node_id=node_id,
116
+ parent_session_id=parent_session_id,
117
+ )
118
+
119
+ editor = ThrottledTranscriptEditor(
120
+ platform=self.platform,
121
+ parse_mode=self._get_parse_mode(),
122
+ get_limit_chars=self._get_limit_chars,
123
+ transcript=transcript,
124
+ render_ctx=render_ctx,
125
+ node_id=node_id,
126
+ chat_id=chat_id,
127
+ status_msg_id=status_msg_id,
128
+ debug_platform_edits=self._debug_platform_edits,
129
+ log_messaging_error_details=self._log_messaging_error_details,
130
+ )
131
+
132
+ async def update_ui(status: str | None = None, force: bool = False) -> None:
133
+ await editor.update(status, force=force)
134
+
135
+ try:
136
+ try:
137
+ (
138
+ cli_session,
139
+ session_or_temp_id,
140
+ is_new,
141
+ ) = await self.cli_manager.get_or_create_session(
142
+ session_id=parent_session_id
143
+ )
144
+ if is_new:
145
+ temp_session_id = session_or_temp_id
146
+ else:
147
+ captured_session_id = session_or_temp_id
148
+
149
+ sess_evt = (
150
+ "claude_cli.session.pending_created"
151
+ if is_new
152
+ else "claude_cli.session.reused"
153
+ )
154
+ trace_event(
155
+ stage="claude_cli",
156
+ event=sess_evt,
157
+ source=platform_nm,
158
+ chat_id=chat_id,
159
+ node_id=node_id,
160
+ status_message_id=status_msg_id,
161
+ session_handle=str(session_or_temp_id),
162
+ parent_resume_session_id=parent_session_id,
163
+ fork_requested=bool(parent_session_id),
164
+ )
165
+ trace_event(
166
+ stage="claude_cli",
167
+ event="claude_cli.request.sent",
168
+ source=platform_nm,
169
+ chat_id=chat_id,
170
+ node_id=node_id,
171
+ prompt=incoming.text,
172
+ fork_session_arg=bool(parent_session_id),
173
+ resume_session_arg=parent_session_id,
174
+ )
175
+ except RuntimeError as e:
176
+ error_message = get_user_facing_error_message(e)
177
+ transcript.apply({"type": "error", "message": error_message})
178
+ await update_ui(
179
+ self._format_status("⏳", "Session limit reached", None),
180
+ force=True,
181
+ )
182
+ if tree:
183
+ await tree.update_state(
184
+ node_id,
185
+ MessageState.ERROR,
186
+ error_message=error_message,
187
+ )
188
+ self._save_tree(tree)
189
+ trace_event(
190
+ stage="claude_cli",
191
+ event="claude_cli.session.limit_reached",
192
+ source=platform_nm,
193
+ chat_id=chat_id,
194
+ node_id=node_id,
195
+ )
196
+ return
197
+
198
+ async for event_data in cli_session.start_task(
199
+ incoming.text,
200
+ session_id=parent_session_id,
201
+ fork_session=bool(parent_session_id),
202
+ ):
203
+ if not isinstance(event_data, dict):
204
+ logger.warning(
205
+ f"HANDLER: Non-dict event received: {type(event_data)}"
206
+ )
207
+ continue
208
+
209
+ (
210
+ captured_session_id,
211
+ temp_session_id,
212
+ ) = await handle_session_info_event(
213
+ event_data,
214
+ tree,
215
+ node_id,
216
+ captured_session_id,
217
+ temp_session_id,
218
+ cli_manager=self.cli_manager,
219
+ session_store=self.session_store,
220
+ )
221
+ if event_data.get("type") == "session_info":
222
+ continue
223
+
224
+ parsed_list = parse_cli_event(
225
+ event_data, log_raw_cli=self._log_raw_cli_diagnostics
226
+ )
227
+
228
+ for parsed in parsed_list:
229
+ (
230
+ last_status,
231
+ had_transcript_events,
232
+ ) = await process_parsed_cli_event(
233
+ parsed,
234
+ transcript,
235
+ update_ui,
236
+ last_status,
237
+ had_transcript_events,
238
+ tree,
239
+ node_id,
240
+ captured_session_id,
241
+ session_store=self.session_store,
242
+ format_status=self._format_status,
243
+ propagate_error_to_children=self.propagate_error_to_children,
244
+ log_messaging_error_details=self._log_messaging_error_details,
245
+ )
246
+
247
+ except asyncio.CancelledError:
248
+ trace_event(
249
+ stage="claude_cli",
250
+ event="turn.processor.cancelled",
251
+ source=platform_nm,
252
+ chat_id=chat_id,
253
+ node_id=node_id,
254
+ )
255
+ logger.warning(f"HANDLER: Task cancelled for node {node_id}")
256
+ cancel_reason = None
257
+ if isinstance(node.context, dict):
258
+ cancel_reason = node.context.get("cancel_reason")
259
+
260
+ if cancel_reason == "stop":
261
+ await update_ui(self._format_status("⏹", "Stopped.", None), force=True)
262
+ else:
263
+ transcript.apply({"type": "error", "message": "Task was cancelled"})
264
+ await update_ui(
265
+ self._format_status("❌", "Cancelled", None), force=True
266
+ )
267
+
268
+ if tree:
269
+ await tree.update_state(
270
+ node_id, MessageState.ERROR, error_message="Cancelled by user"
271
+ )
272
+ self._save_tree(tree)
273
+ except Exception as e:
274
+ trace_event(
275
+ stage="claude_cli",
276
+ event="turn.processor.exception",
277
+ source=platform_nm,
278
+ chat_id=chat_id,
279
+ node_id=node_id,
280
+ exc_type=type(e).__name__,
281
+ )
282
+ logger.error(
283
+ "HANDLER: Task failed with exception: {}",
284
+ format_exception_for_log(
285
+ e, log_full_message=self._log_messaging_error_details
286
+ ),
287
+ )
288
+ error_msg = format_user_error_preview(e)
289
+ transcript.apply({"type": "error", "message": error_msg})
290
+ await update_ui(self._format_status("💥", "Task Failed", None), force=True)
291
+ if tree:
292
+ await self.propagate_error_to_children(
293
+ node_id, error_msg, "Parent task failed"
294
+ )
295
+ finally:
296
+ trace_event(
297
+ stage="routing",
298
+ event="turn.processor.finished",
299
+ source=platform_nm,
300
+ chat_id=chat_id,
301
+ node_id=node_id,
302
+ claude_session_id=captured_session_id or temp_session_id,
303
+ )
304
+ try:
305
+ if captured_session_id:
306
+ await self.cli_manager.remove_session(captured_session_id)
307
+ elif temp_session_id:
308
+ await self.cli_manager.remove_session(temp_session_id)
309
+ except Exception as e:
310
+ logger.debug(
311
+ "Failed to remove session for node {}: {}",
312
+ node_id,
313
+ format_exception_for_log(
314
+ e, log_full_message=self._log_messaging_error_details
315
+ ),
316
+ )
317
+
318
+ async def propagate_error_to_children(
319
+ self,
320
+ node_id: str,
321
+ error_msg: str,
322
+ child_status_text: str,
323
+ ) -> None:
324
+ """Mark node as error and propagate to pending children with UI updates."""
325
+ tree_queue = self._get_tree_queue()
326
+ affected = await tree_queue.mark_node_error(
327
+ node_id, error_msg, propagate_to_children=True
328
+ )
329
+ if affected:
330
+ self._save_tree(tree_queue.get_tree_for_node(node_id))
331
+ for child in affected[1:]:
332
+ self.platform.fire_and_forget(
333
+ self.platform.queue_edit_message(
334
+ child.incoming.chat_id,
335
+ child.status_message_id,
336
+ self._format_status("❌", "Cancelled:", child_status_text),
337
+ parse_mode=self._get_parse_mode(),
338
+ )
339
+ )
340
+
341
+
342
+ __all__ = ["MessagingNodeRunner"]
@@ -0,0 +1,15 @@
1
+ """Messaging platform adapters (Telegram, Discord, etc.)."""
2
+
3
+ from .base import (
4
+ ManagedClaudeSessionManagerProtocol,
5
+ ManagedClaudeSessionProtocol,
6
+ MessagingPlatform,
7
+ )
8
+ from .factory import create_messaging_platform
9
+
10
+ __all__ = [
11
+ "ManagedClaudeSessionManagerProtocol",
12
+ "ManagedClaudeSessionProtocol",
13
+ "MessagingPlatform",
14
+ "create_messaging_platform",
15
+ ]
@@ -0,0 +1,228 @@
1
+ """Abstract base class for messaging platforms."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncGenerator, Awaitable, Callable
5
+ from typing import (
6
+ Any,
7
+ Protocol,
8
+ runtime_checkable,
9
+ )
10
+
11
+ from ..models import IncomingMessage
12
+
13
+
14
+ @runtime_checkable
15
+ class ManagedClaudeSessionProtocol(Protocol):
16
+ """Protocol for managed Claude sessions - avoid circular imports."""
17
+
18
+ def start_task(
19
+ self, prompt: str, session_id: str | None = None, fork_session: bool = False
20
+ ) -> AsyncGenerator[dict, Any]: ...
21
+
22
+ @property
23
+ def is_busy(self) -> bool: ...
24
+
25
+
26
+ @runtime_checkable
27
+ class ManagedClaudeSessionManagerProtocol(Protocol):
28
+ """
29
+ Protocol for managed Claude session managers to avoid tight coupling.
30
+ """
31
+
32
+ async def get_or_create_session(
33
+ self, session_id: str | None = None
34
+ ) -> tuple[ManagedClaudeSessionProtocol, str, bool]:
35
+ """
36
+ Get an existing session or create a new one.
37
+
38
+ Returns: Tuple of (session, session_id, is_new_session)
39
+ """
40
+ ...
41
+
42
+ async def register_real_session_id(
43
+ self, temp_id: str, real_session_id: str
44
+ ) -> bool:
45
+ """Register the real session ID from CLI output."""
46
+ ...
47
+
48
+ async def stop_all(self) -> None:
49
+ """Stop all sessions."""
50
+ ...
51
+
52
+ async def remove_session(self, session_id: str) -> bool:
53
+ """Remove a session from the manager."""
54
+ ...
55
+
56
+ def get_stats(self) -> dict:
57
+ """Get session statistics."""
58
+ ...
59
+
60
+
61
+ class MessagingPlatform(ABC):
62
+ """
63
+ Base class for all messaging platform adapters.
64
+
65
+ Implement this to add support for Telegram, Discord, Slack, etc.
66
+ """
67
+
68
+ name: str = "base"
69
+
70
+ @abstractmethod
71
+ async def start(self) -> None:
72
+ """Initialize and connect to the messaging platform."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ async def stop(self) -> None:
77
+ """Disconnect and cleanup resources."""
78
+ pass
79
+
80
+ @abstractmethod
81
+ async def send_message(
82
+ self,
83
+ chat_id: str,
84
+ text: str,
85
+ reply_to: str | None = None,
86
+ parse_mode: str | None = None,
87
+ message_thread_id: str | None = None,
88
+ ) -> str:
89
+ """
90
+ Send a message to a chat.
91
+
92
+ Args:
93
+ chat_id: The chat/channel ID to send to
94
+ text: Message content
95
+ reply_to: Optional message ID to reply to
96
+ parse_mode: Optional formatting mode ("markdown", "html")
97
+ message_thread_id: Optional thread or topic id for threaded channels
98
+ (e.g. forum topics); unused on platforms that do not support it.
99
+
100
+ Returns:
101
+ The message ID of the sent message
102
+ """
103
+ pass
104
+
105
+ @abstractmethod
106
+ async def edit_message(
107
+ self,
108
+ chat_id: str,
109
+ message_id: str,
110
+ text: str,
111
+ parse_mode: str | None = None,
112
+ ) -> None:
113
+ """
114
+ Edit an existing message.
115
+
116
+ Args:
117
+ chat_id: The chat/channel ID
118
+ message_id: The message ID to edit
119
+ text: New message content
120
+ parse_mode: Optional formatting mode
121
+ """
122
+ pass
123
+
124
+ @abstractmethod
125
+ async def delete_message(
126
+ self,
127
+ chat_id: str,
128
+ message_id: str,
129
+ ) -> None:
130
+ """
131
+ Delete a message from a chat.
132
+
133
+ Args:
134
+ chat_id: The chat/channel ID
135
+ message_id: The message ID to delete
136
+ """
137
+ pass
138
+
139
+ @abstractmethod
140
+ async def queue_send_message(
141
+ self,
142
+ chat_id: str,
143
+ text: str,
144
+ reply_to: str | None = None,
145
+ parse_mode: str | None = None,
146
+ fire_and_forget: bool = True,
147
+ message_thread_id: str | None = None,
148
+ ) -> str | None:
149
+ """
150
+ Enqueue a message to be sent.
151
+
152
+ If fire_and_forget is True, returns None immediately.
153
+ Otherwise, waits for the rate limiter and returns message ID.
154
+ """
155
+ pass
156
+
157
+ @abstractmethod
158
+ async def queue_edit_message(
159
+ self,
160
+ chat_id: str,
161
+ message_id: str,
162
+ text: str,
163
+ parse_mode: str | None = None,
164
+ fire_and_forget: bool = True,
165
+ ) -> None:
166
+ """
167
+ Enqueue a message edit.
168
+
169
+ If fire_and_forget is True, returns immediately.
170
+ Otherwise, waits for the rate limiter.
171
+ """
172
+ pass
173
+
174
+ @abstractmethod
175
+ async def queue_delete_message(
176
+ self,
177
+ chat_id: str,
178
+ message_id: str,
179
+ fire_and_forget: bool = True,
180
+ ) -> None:
181
+ """
182
+ Enqueue a message deletion.
183
+
184
+ If fire_and_forget is True, returns immediately.
185
+ Otherwise, waits for the rate limiter.
186
+ """
187
+ pass
188
+
189
+ async def queue_delete_messages(
190
+ self,
191
+ chat_id: str,
192
+ message_ids: list[str],
193
+ *,
194
+ fire_and_forget: bool = True,
195
+ ) -> None:
196
+ """Delete many messages; default loops :meth:`queue_delete_message`.
197
+
198
+ Adapters with native bulk delete should override.
199
+ """
200
+ for mid in message_ids:
201
+ await self.queue_delete_message(
202
+ chat_id, mid, fire_and_forget=fire_and_forget
203
+ )
204
+
205
+ @abstractmethod
206
+ def on_message(
207
+ self,
208
+ handler: Callable[[IncomingMessage], Awaitable[None]],
209
+ ) -> None:
210
+ """
211
+ Register a message handler callback.
212
+
213
+ The handler will be called for each incoming message.
214
+
215
+ Args:
216
+ handler: Async function that processes incoming messages
217
+ """
218
+ pass
219
+
220
+ @abstractmethod
221
+ def fire_and_forget(self, task: Awaitable[Any]) -> None:
222
+ """Execute a coroutine without awaiting it."""
223
+ pass
224
+
225
+ @property
226
+ def is_connected(self) -> bool:
227
+ """Check if the platform is connected."""
228
+ return False