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,55 @@
1
+ """Platform rendering profiles for messaging transcripts and status text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+
8
+ from messaging.rendering.discord_markdown import (
9
+ discord_bold,
10
+ discord_code_inline,
11
+ escape_discord,
12
+ escape_discord_code,
13
+ render_markdown_to_discord,
14
+ )
15
+ from messaging.rendering.discord_markdown import (
16
+ format_status as format_status_discord,
17
+ )
18
+ from messaging.rendering.telegram_markdown import (
19
+ escape_md_v2,
20
+ escape_md_v2_code,
21
+ mdv2_bold,
22
+ mdv2_code_inline,
23
+ render_markdown_to_mdv2,
24
+ )
25
+ from messaging.rendering.telegram_markdown import (
26
+ format_status as format_status_telegram,
27
+ )
28
+ from messaging.transcript import RenderCtx
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class RenderingProfile:
33
+ format_status: Callable[[str, str, str | None], str]
34
+ parse_mode: str | None
35
+ render_ctx: RenderCtx
36
+ limit_chars: int
37
+
38
+
39
+ def build_rendering_profile(platform_name: str) -> RenderingProfile:
40
+ """Return rendering rules for a messaging platform."""
41
+ is_discord = platform_name == "discord"
42
+ return RenderingProfile(
43
+ format_status=format_status_discord if is_discord else format_status_telegram,
44
+ parse_mode=None if is_discord else "MarkdownV2",
45
+ render_ctx=RenderCtx(
46
+ bold=discord_bold if is_discord else mdv2_bold,
47
+ code_inline=discord_code_inline if is_discord else mdv2_code_inline,
48
+ escape_code=escape_discord_code if is_discord else escape_md_v2_code,
49
+ escape_text=escape_discord if is_discord else escape_md_v2,
50
+ render_markdown=render_markdown_to_discord
51
+ if is_discord
52
+ else render_markdown_to_mdv2,
53
+ ),
54
+ limit_chars=1900 if is_discord else 3900,
55
+ )
@@ -0,0 +1,327 @@
1
+ """Telegram MarkdownV2 utilities.
2
+
3
+ Renders common Markdown into Telegram MarkdownV2 format.
4
+ Used by the message handler and Telegram platform adapter.
5
+ """
6
+
7
+ from markdown_it import MarkdownIt
8
+
9
+ from .markdown_tables import normalize_gfm_tables
10
+
11
+ MDV2_SPECIAL_CHARS = set("\\_*[]()~`>#+-=|{}.!")
12
+ MDV2_LINK_ESCAPE = set("\\)")
13
+
14
+ _MD = MarkdownIt("commonmark", {"html": False, "breaks": False})
15
+ _MD.enable("strikethrough")
16
+ _MD.enable("table")
17
+
18
+
19
+ def escape_md_v2(text: str) -> str:
20
+ """Escape text for Telegram MarkdownV2."""
21
+ return "".join(f"\\{ch}" if ch in MDV2_SPECIAL_CHARS else ch for ch in text)
22
+
23
+
24
+ def escape_md_v2_code(text: str) -> str:
25
+ """Escape text for Telegram MarkdownV2 code spans/blocks."""
26
+ return text.replace("\\", "\\\\").replace("`", "\\`")
27
+
28
+
29
+ def escape_md_v2_link_url(text: str) -> str:
30
+ """Escape URL for Telegram MarkdownV2 link destination."""
31
+ return "".join(f"\\{ch}" if ch in MDV2_LINK_ESCAPE else ch for ch in text)
32
+
33
+
34
+ def mdv2_bold(text: str) -> str:
35
+ """Format text as bold in MarkdownV2."""
36
+ return f"*{escape_md_v2(text)}*"
37
+
38
+
39
+ def mdv2_code_inline(text: str) -> str:
40
+ """Format text as inline code in MarkdownV2."""
41
+ return f"`{escape_md_v2_code(text)}`"
42
+
43
+
44
+ def format_status(emoji: str, label: str, suffix: str | None = None) -> str:
45
+ """Format a status message with emoji and optional suffix."""
46
+ base = f"{emoji} {mdv2_bold(label)}"
47
+ if suffix:
48
+ return f"{base} {escape_md_v2(suffix)}"
49
+ return base
50
+
51
+
52
+ def render_markdown_to_mdv2(text: str) -> str:
53
+ """Render common Markdown into Telegram MarkdownV2."""
54
+ if not text:
55
+ return ""
56
+
57
+ text = normalize_gfm_tables(text)
58
+ tokens = _MD.parse(text)
59
+
60
+ def render_inline_table_plain(children) -> str:
61
+ out: list[str] = []
62
+ for tok in children:
63
+ if tok.type == "text" or tok.type == "code_inline":
64
+ out.append(tok.content)
65
+ elif tok.type in {"softbreak", "hardbreak"}:
66
+ out.append(" ")
67
+ elif tok.type == "image" and tok.content:
68
+ out.append(tok.content)
69
+ return "".join(out)
70
+
71
+ def render_inline_plain(children) -> str:
72
+ out: list[str] = []
73
+ for tok in children:
74
+ if tok.type == "text" or tok.type == "code_inline":
75
+ out.append(escape_md_v2(tok.content))
76
+ elif tok.type in {"softbreak", "hardbreak"}:
77
+ out.append("\n")
78
+ return "".join(out)
79
+
80
+ def render_inline(children) -> str:
81
+ out: list[str] = []
82
+ i = 0
83
+ while i < len(children):
84
+ tok = children[i]
85
+ t = tok.type
86
+ if t == "text":
87
+ out.append(escape_md_v2(tok.content))
88
+ elif t in {"softbreak", "hardbreak"}:
89
+ out.append("\n")
90
+ elif t == "em_open" or t == "em_close":
91
+ out.append("_")
92
+ elif t == "strong_open" or t == "strong_close":
93
+ out.append("*")
94
+ elif t == "s_open" or t == "s_close":
95
+ out.append("~")
96
+ elif t == "code_inline":
97
+ out.append(f"`{escape_md_v2_code(tok.content)}`")
98
+ elif t == "link_open":
99
+ href = ""
100
+ if tok.attrs:
101
+ if isinstance(tok.attrs, dict):
102
+ href = tok.attrs.get("href", "")
103
+ else:
104
+ for key, val in tok.attrs:
105
+ if key == "href":
106
+ href = val
107
+ break
108
+ inner_tokens = []
109
+ i += 1
110
+ while i < len(children) and children[i].type != "link_close":
111
+ inner_tokens.append(children[i])
112
+ i += 1
113
+ link_text = ""
114
+ for child in inner_tokens:
115
+ if child.type == "text" or child.type == "code_inline":
116
+ link_text += child.content
117
+ out.append(
118
+ f"[{escape_md_v2(link_text)}]({escape_md_v2_link_url(href)})"
119
+ )
120
+ elif t == "image":
121
+ href = ""
122
+ alt = tok.content or ""
123
+ if tok.attrs:
124
+ if isinstance(tok.attrs, dict):
125
+ href = tok.attrs.get("src", "")
126
+ else:
127
+ for key, val in tok.attrs:
128
+ if key == "src":
129
+ href = val
130
+ break
131
+ if alt:
132
+ out.append(f"{escape_md_v2(alt)} ({escape_md_v2_link_url(href)})")
133
+ else:
134
+ out.append(escape_md_v2_link_url(href))
135
+ else:
136
+ out.append(escape_md_v2(tok.content or ""))
137
+ i += 1
138
+ return "".join(out)
139
+
140
+ out: list[str] = []
141
+ list_stack: list[dict] = []
142
+ pending_prefix: str | None = None
143
+ blockquote_level = 0
144
+ in_heading = False
145
+
146
+ def apply_blockquote(val: str) -> str:
147
+ if blockquote_level <= 0:
148
+ return val
149
+ prefix = "> " * blockquote_level
150
+ return prefix + val.replace("\n", "\n" + prefix)
151
+
152
+ i = 0
153
+ while i < len(tokens):
154
+ tok = tokens[i]
155
+ t = tok.type
156
+ if t == "paragraph_open":
157
+ pass
158
+ elif t == "paragraph_close":
159
+ out.append("\n")
160
+ elif t == "heading_open":
161
+ in_heading = True
162
+ elif t == "heading_close":
163
+ in_heading = False
164
+ out.append("\n")
165
+ elif t == "bullet_list_open":
166
+ list_stack.append({"type": "bullet", "index": 1})
167
+ elif t == "bullet_list_close":
168
+ if list_stack:
169
+ list_stack.pop()
170
+ out.append("\n")
171
+ elif t == "ordered_list_open":
172
+ start = 1
173
+ if tok.attrs:
174
+ if isinstance(tok.attrs, dict):
175
+ val = tok.attrs.get("start")
176
+ if val is not None:
177
+ try:
178
+ start = int(val)
179
+ except TypeError, ValueError:
180
+ start = 1
181
+ else:
182
+ for key, val in tok.attrs:
183
+ if key == "start":
184
+ try:
185
+ start = int(val)
186
+ except TypeError, ValueError:
187
+ start = 1
188
+ break
189
+ list_stack.append({"type": "ordered", "index": start})
190
+ elif t == "ordered_list_close":
191
+ if list_stack:
192
+ list_stack.pop()
193
+ out.append("\n")
194
+ elif t == "list_item_open":
195
+ if list_stack:
196
+ top = list_stack[-1]
197
+ if top["type"] == "bullet":
198
+ pending_prefix = "\\- "
199
+ else:
200
+ pending_prefix = f"{top['index']}\\."
201
+ top["index"] += 1
202
+ pending_prefix += " "
203
+ elif t == "list_item_close":
204
+ out.append("\n")
205
+ elif t == "blockquote_open":
206
+ blockquote_level += 1
207
+ elif t == "blockquote_close":
208
+ blockquote_level = max(0, blockquote_level - 1)
209
+ out.append("\n")
210
+ elif t == "table_open":
211
+ if pending_prefix:
212
+ out.append(apply_blockquote(pending_prefix.rstrip()))
213
+ out.append("\n")
214
+ pending_prefix = None
215
+
216
+ rows: list[list[str]] = []
217
+ row_is_header: list[bool] = []
218
+
219
+ j = i + 1
220
+ in_thead = False
221
+ in_row = False
222
+ current_row: list[str] = []
223
+ current_row_header = False
224
+
225
+ in_cell = False
226
+ cell_parts: list[str] = []
227
+
228
+ while j < len(tokens):
229
+ tt = tokens[j].type
230
+ if tt == "thead_open":
231
+ in_thead = True
232
+ elif tt == "thead_close":
233
+ in_thead = False
234
+ elif tt == "tr_open":
235
+ in_row = True
236
+ current_row = []
237
+ current_row_header = in_thead
238
+ elif tt in {"th_open", "td_open"}:
239
+ in_cell = True
240
+ cell_parts = []
241
+ elif tt == "inline" and in_cell:
242
+ cell_parts.append(
243
+ render_inline_table_plain(tokens[j].children or [])
244
+ )
245
+ elif tt in {"th_close", "td_close"} and in_cell:
246
+ cell = " ".join(cell_parts).strip()
247
+ current_row.append(cell)
248
+ in_cell = False
249
+ cell_parts = []
250
+ elif tt == "tr_close" and in_row:
251
+ rows.append(current_row)
252
+ row_is_header.append(bool(current_row_header))
253
+ in_row = False
254
+ elif tt == "table_close":
255
+ break
256
+ j += 1
257
+
258
+ if rows:
259
+ col_count = max((len(r) for r in rows), default=0)
260
+ norm_rows: list[list[str]] = []
261
+ for r in rows:
262
+ if len(r) < col_count:
263
+ r = r + [""] * (col_count - len(r))
264
+ norm_rows.append(r)
265
+
266
+ widths: list[int] = []
267
+ for c in range(col_count):
268
+ w = max((len(r[c]) for r in norm_rows), default=0)
269
+ widths.append(max(w, 3))
270
+
271
+ def fmt_row(
272
+ r: list[str], _w: list[int] = widths, _c: int = col_count
273
+ ) -> str:
274
+ cells = [r[c].ljust(_w[c]) for c in range(_c)]
275
+ return "| " + " | ".join(cells) + " |"
276
+
277
+ def fmt_sep(_w: list[int] = widths, _c: int = col_count) -> str:
278
+ cells = ["-" * _w[c] for c in range(_c)]
279
+ return "| " + " | ".join(cells) + " |"
280
+
281
+ last_header_idx = -1
282
+ for idx, is_h in enumerate(row_is_header):
283
+ if is_h:
284
+ last_header_idx = idx
285
+
286
+ lines: list[str] = []
287
+ for idx, r in enumerate(norm_rows):
288
+ lines.append(fmt_row(r))
289
+ if idx == last_header_idx:
290
+ lines.append(fmt_sep())
291
+
292
+ table_text = "\n".join(lines).rstrip()
293
+ out.append(f"```\n{escape_md_v2_code(table_text)}\n```")
294
+ out.append("\n")
295
+
296
+ i = j + 1
297
+ continue
298
+ elif t in {"code_block", "fence"}:
299
+ code = escape_md_v2_code(tok.content.rstrip("\n"))
300
+ out.append(f"```\n{code}\n```")
301
+ out.append("\n")
302
+ elif t == "inline":
303
+ rendered = render_inline(tok.children or [])
304
+ if in_heading:
305
+ rendered = f"*{render_inline_plain(tok.children or [])}*"
306
+ if pending_prefix:
307
+ rendered = pending_prefix + rendered
308
+ pending_prefix = None
309
+ rendered = apply_blockquote(rendered)
310
+ out.append(rendered)
311
+ else:
312
+ if tok.content:
313
+ out.append(escape_md_v2(tok.content))
314
+ i += 1
315
+
316
+ return "".join(out).rstrip()
317
+
318
+
319
+ __all__ = [
320
+ "escape_md_v2",
321
+ "escape_md_v2_code",
322
+ "escape_md_v2_link_url",
323
+ "format_status",
324
+ "mdv2_bold",
325
+ "mdv2_code_inline",
326
+ "render_markdown_to_mdv2",
327
+ ]
@@ -0,0 +1,17 @@
1
+ """Helpers for redacting user-derived content from log lines."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def format_exception_for_log(exc: BaseException, *, log_full_message: bool) -> str:
7
+ """Return exception type and optionally ``str(exc)`` for operator diagnostics."""
8
+ if log_full_message:
9
+ return f"{type(exc).__name__}: {exc}"
10
+ return type(exc).__name__
11
+
12
+
13
+ def text_len_hint(text: str | None) -> int:
14
+ """Length of text for metadata-only logging (0 when missing)."""
15
+ if not text:
16
+ return 0
17
+ return len(text)