monkeybot 2.1.1__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.
- monkeybot/__init__.py +3 -0
- monkeybot/cli/__init__.py +3 -0
- monkeybot/cli/__main__.py +8 -0
- monkeybot/cli/audio_io.py +8 -0
- monkeybot/cli/gateway_manager.py +17 -0
- monkeybot/cli/main.py +22 -0
- monkeybot/cli/push_to_talk.py +12 -0
- monkeybot/cli/realtime_client.py +13 -0
- monkeybot/core/__init__.py +19 -0
- monkeybot/core/attachments/__init__.py +22 -0
- monkeybot/core/attachments/catalog.py +62 -0
- monkeybot/core/attachments/config.py +52 -0
- monkeybot/core/attachments/freeze.py +158 -0
- monkeybot/core/attachments/resolve.py +70 -0
- monkeybot/core/attachments/store.py +180 -0
- monkeybot/core/attachments/text.py +72 -0
- monkeybot/core/attachments/tools.py +54 -0
- monkeybot/core/bootstrap.py +242 -0
- monkeybot/core/config/__init__.py +71 -0
- monkeybot/core/config/realtime_config.py +150 -0
- monkeybot/core/config/runtime_env.py +262 -0
- monkeybot/core/config/settings.py +341 -0
- monkeybot/core/config/validation.py +249 -0
- monkeybot/core/config/yaml_loader.py +45 -0
- monkeybot/core/context/__init__.py +781 -0
- monkeybot/core/context/campaign_context.py +8 -0
- monkeybot/core/context/common.py +14 -0
- monkeybot/core/context/curator.py +255 -0
- monkeybot/core/context/epoch.py +226 -0
- monkeybot/core/context/memory_prompt.py +222 -0
- monkeybot/core/context/tool_output_policy.py +270 -0
- monkeybot/core/context/tool_result_ingress.py +290 -0
- monkeybot/core/context/tool_shapers.py +361 -0
- monkeybot/core/hooks/__init__.py +261 -0
- monkeybot/core/llm/__init__.py +4 -0
- monkeybot/core/llm/provider.py +296 -0
- monkeybot/core/llm/realtime_provider.py +203 -0
- monkeybot/core/llm/usage.py +57 -0
- monkeybot/core/logging_utils.py +24 -0
- monkeybot/core/mcp/__init__.py +1 -0
- monkeybot/core/mcp/mcp_client.py +1215 -0
- monkeybot/core/mcp/ports_mcp.py +109 -0
- monkeybot/core/memory/__init__.py +24 -0
- monkeybot/core/memory/hook.py +413 -0
- monkeybot/core/memory/index_format.py +104 -0
- monkeybot/core/memory/integrity.py +180 -0
- monkeybot/core/memory/organizer.py +270 -0
- monkeybot/core/memory/storage_ops.py +139 -0
- monkeybot/core/memory/subsystem.py +91 -0
- monkeybot/core/messages/__init__.py +16 -0
- monkeybot/core/messages/convert_provider.py +41 -0
- monkeybot/core/messages/tool_integrity.py +262 -0
- monkeybot/core/messages/transform_context.py +84 -0
- monkeybot/core/path_safety.py +11 -0
- monkeybot/core/persistence/__init__.py +17 -0
- monkeybot/core/persistence/backends.py +236 -0
- monkeybot/core/persistence/db.py +28 -0
- monkeybot/core/persistence/durable_runs.py +286 -0
- monkeybot/core/persistence/firestore.py +658 -0
- monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
- monkeybot/core/persistence/history.py +156 -0
- monkeybot/core/persistence/postgres.py +895 -0
- monkeybot/core/persistence/runs.py +76 -0
- monkeybot/core/persistence/scheduled_loops.py +435 -0
- monkeybot/core/persistence/session_turn_locks.py +94 -0
- monkeybot/core/persistence/sqlite.py +218 -0
- monkeybot/core/persistence/sqlite_backend.py +74 -0
- monkeybot/core/persistence/thread_summary.py +61 -0
- monkeybot/core/persistence/transcript.py +194 -0
- monkeybot/core/persistence/usage.py +149 -0
- monkeybot/core/prompts/__init__.py +1 -0
- monkeybot/core/prompts/harness_prompt.py +197 -0
- monkeybot/core/prompts/prompt.py +215 -0
- monkeybot/core/runtime/__init__.py +1 -0
- monkeybot/core/runtime/context_budget.py +267 -0
- monkeybot/core/runtime/events.py +819 -0
- monkeybot/core/runtime/input_admission.py +154 -0
- monkeybot/core/runtime/loop.py +2374 -0
- monkeybot/core/runtime/provider_stream_mapper.py +159 -0
- monkeybot/core/runtime/realtime_loop.py +654 -0
- monkeybot/core/runtime/utterance_buffer.py +179 -0
- monkeybot/core/subagents/__init__.py +1 -0
- monkeybot/core/subagents/subagent_proto.py +331 -0
- monkeybot/core/subagents/subagent_worker.py +441 -0
- monkeybot/core/subagents/worker_pool.py +403 -0
- monkeybot/core/testing/__init__.py +1 -0
- monkeybot/core/testing/mocks_provider.py +86 -0
- monkeybot/core/testing/mocks_realtime_provider.py +137 -0
- monkeybot/core/tools/__init__.py +1 -0
- monkeybot/core/tools/core_tool_executor.py +1548 -0
- monkeybot/core/tools/inspector.py +226 -0
- monkeybot/core/tools/loop_inspector.py +45 -0
- monkeybot/core/tools/patch.py +480 -0
- monkeybot/core/tools/permission.py +284 -0
- monkeybot/core/tools/sandbox_executor.py +255 -0
- monkeybot/core/tools/spill_inventory.py +35 -0
- monkeybot/core/tools/terminal.py +381 -0
- monkeybot/core/tools/text_normalize.py +25 -0
- monkeybot/core/tools/types.py +33 -0
- monkeybot/core/tools/workspace_service.py +710 -0
- monkeybot/core/tools/workspace_tools.py +116 -0
- monkeybot/core/types/__init__.py +1 -0
- monkeybot/core/types/content_blocks.py +644 -0
- monkeybot/core/types/interfaces.py +156 -0
- monkeybot/core/types/types_tools.py +29 -0
- monkeybot/core/workspace/__init__.py +8 -0
- monkeybot/core/workspace/factory.py +45 -0
- monkeybot/core/workspace/gcs.py +130 -0
- monkeybot/core/workspace/local.py +162 -0
- monkeybot/core/workspace/protocol.py +45 -0
- monkeybot/core/workspace/s3.py +151 -0
- monkeybot/core/workspace_layout.py +27 -0
- monkeybot/gateway/__init__.py +1 -0
- monkeybot/gateway/bootstrap.py +18 -0
- monkeybot/gateway/main.py +47 -0
- monkeybot/gateway/realtime/__init__.py +31 -0
- monkeybot/gateway/realtime/app.py +321 -0
- monkeybot/gateway/realtime/deps.py +52 -0
- monkeybot/gateway/realtime/errors.py +81 -0
- monkeybot/gateway/realtime/guardrails.py +88 -0
- monkeybot/gateway/realtime/manager.py +77 -0
- monkeybot/gateway/realtime/metrics.py +144 -0
- monkeybot/gateway/realtime/routes.py +864 -0
- monkeybot/gateway/realtime/session.py +232 -0
- monkeybot/gateway/realtime/wire.py +412 -0
- monkeybot/gateway/realtime_main.py +49 -0
- monkeybot/gateway/sse/__init__.py +1 -0
- monkeybot/gateway/sse/app.py +733 -0
- monkeybot/gateway/sse/loop_port.py +31 -0
- monkeybot/gateway/sse/models.py +177 -0
- monkeybot/gateway/sse/reply_body.py +91 -0
- monkeybot/gateway/sse/routes.py +1101 -0
- monkeybot/gateway/sse/scheduler_routes.py +200 -0
- monkeybot/gateway/sse/scheduler_wiring.py +96 -0
- monkeybot/gateway/sse/session_bus.py +226 -0
- monkeybot/gateway/sse/sse.py +46 -0
- monkeybot/gateway/sse/workspace_layout.py +7 -0
- monkeybot/observability/__init__.py +220 -0
- monkeybot/observability/_state.py +10 -0
- monkeybot/observability/instrumentation.py +153 -0
- monkeybot/observability/propagation.py +65 -0
- monkeybot/observability/spans.py +455 -0
- monkeybot/providers/__init__.py +19 -0
- monkeybot/providers/_openai_compat.py +450 -0
- monkeybot/providers/_utils.py +473 -0
- monkeybot/providers/bedrock.py +145 -0
- monkeybot/providers/claude.py +125 -0
- monkeybot/providers/gemini.py +677 -0
- monkeybot/providers/gemini_live.py +398 -0
- monkeybot/providers/huggingface.py +129 -0
- monkeybot/providers/nvidia.py +104 -0
- monkeybot/providers/ollama.py +152 -0
- monkeybot/providers/openai.py +127 -0
- monkeybot/providers/pricing.py +60 -0
- monkeybot/providers/sampling.py +44 -0
- monkeybot/providers/vertex_claude.py +148 -0
- monkeybot/scaffold/__init__.py +33 -0
- monkeybot/scheduler/__init__.py +13 -0
- monkeybot/scheduler/__main__.py +4 -0
- monkeybot/scheduler/engine.py +333 -0
- monkeybot/scheduler/http_invoker.py +61 -0
- monkeybot/scheduler/interval.py +77 -0
- monkeybot/scheduler/tick_result.py +34 -0
- monkeybot/scheduler/worker.py +87 -0
- monkeybot/subagents/__init__.py +1 -0
- monkeybot/subagents/worker/__init__.py +1 -0
- monkeybot/subagents/worker/__main__.py +22 -0
- monkeybot/web_search/__init__.py +82 -0
- monkeybot/web_search/backends/__init__.py +5 -0
- monkeybot/web_search/backends/duckduckgo.py +32 -0
- monkeybot/web_search/backends/firecrawl.py +43 -0
- monkeybot/web_search/backends/tavily.py +45 -0
- monkeybot/web_search/protocol.py +25 -0
- monkeybot/web_search/tool.py +56 -0
- monkeybot-2.1.1.dist-info/METADATA +318 -0
- monkeybot-2.1.1.dist-info/RECORD +178 -0
- monkeybot-2.1.1.dist-info/WHEEL +4 -0
- monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Injectable ports between the SSE gateway and the agent loop / usage store.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
|
|
7
|
+
from monkeybot.core.types.content_blocks import ContentBlock
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoopPort(Protocol):
|
|
11
|
+
"""Schedules a turn; implementations publish Agent events to the session bus."""
|
|
12
|
+
|
|
13
|
+
async def start_turn(
|
|
14
|
+
self,
|
|
15
|
+
session_id: str,
|
|
16
|
+
request_id: str,
|
|
17
|
+
user_content: list[ContentBlock],
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Schedule background work; events MUST be published to that session's bus."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UsagePort(Protocol):
|
|
23
|
+
"""Returns usage aggregates for GET /sessions/{id}/usage."""
|
|
24
|
+
|
|
25
|
+
async def session_usage(
|
|
26
|
+
self,
|
|
27
|
+
session_id: str,
|
|
28
|
+
*,
|
|
29
|
+
since: str | None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Return JSON-serializable dict matching GET /usage body."""
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models and API errors for the v2 SSE gateway.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ErrorBody(BaseModel):
|
|
11
|
+
"""Error payload matching design contract 1b."""
|
|
12
|
+
|
|
13
|
+
code: str
|
|
14
|
+
message: str
|
|
15
|
+
request_id: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ErrorEnvelope(BaseModel):
|
|
19
|
+
"""Top-level error response `{ \"error\": ... }`."""
|
|
20
|
+
|
|
21
|
+
error: ErrorBody
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APIError(Exception):
|
|
25
|
+
"""Raised for API failures; maps to JSON error envelope via exception handler."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, status_code: int, code: str, message: str, request_id: str) -> None:
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.code = code
|
|
31
|
+
self.message = message
|
|
32
|
+
self.request_id = request_id
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CreateSessionRequest(BaseModel):
|
|
36
|
+
"""POST /sessions body."""
|
|
37
|
+
|
|
38
|
+
session_id: str | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
max_length=128,
|
|
41
|
+
description="Client id; server generates UUID when omitted.",
|
|
42
|
+
)
|
|
43
|
+
agent_md: str | None = Field(default=None, description="Optional AGENT.md path override.")
|
|
44
|
+
model_provider: str | None = Field(
|
|
45
|
+
default=None,
|
|
46
|
+
description="Canonical or alias provider id (e.g. 'openai', 'gemini', 'vertex-claude'). None → server env default.",
|
|
47
|
+
)
|
|
48
|
+
model_name: str | None = Field(
|
|
49
|
+
default=None,
|
|
50
|
+
max_length=200,
|
|
51
|
+
description="Model id for the chosen provider. None → server env default.",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CreateSessionResponse(BaseModel):
|
|
56
|
+
"""POST /sessions response (201)."""
|
|
57
|
+
|
|
58
|
+
session_id: str
|
|
59
|
+
created_at: int = Field(..., description="Unix time in milliseconds.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ReplyBodyFields(BaseModel):
|
|
63
|
+
"""Shared ``message`` / ``content`` fields for reply, steer, and queue."""
|
|
64
|
+
|
|
65
|
+
message: str | None = Field(default=None, max_length=32000)
|
|
66
|
+
content: list[dict[str, Any]] | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ReplyRequest(ReplyBodyFields):
|
|
70
|
+
"""POST /sessions/{id}/reply body."""
|
|
71
|
+
|
|
72
|
+
request_id: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SteerRequest(ReplyBodyFields):
|
|
76
|
+
"""POST /sessions/{id}/steer — mid-turn user injection (requires busy session)."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class QueueRequest(ReplyBodyFields):
|
|
80
|
+
"""POST /sessions/{id}/queue — follow-up prompt drained when the session goes idle."""
|
|
81
|
+
|
|
82
|
+
request_id: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AttachmentUploadResponse(BaseModel):
|
|
86
|
+
"""POST /sessions/{id}/attachments response (201)."""
|
|
87
|
+
|
|
88
|
+
attachment_id: str
|
|
89
|
+
mime_type: str
|
|
90
|
+
size_bytes: int
|
|
91
|
+
filename: str
|
|
92
|
+
created_at: int
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ReplyResponse(BaseModel):
|
|
96
|
+
"""POST /sessions/{id}/reply response (200)."""
|
|
97
|
+
|
|
98
|
+
request_id: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AdmissionAcceptedResponse(BaseModel):
|
|
102
|
+
"""POST /steer or /queue acceptance."""
|
|
103
|
+
|
|
104
|
+
request_id: str
|
|
105
|
+
queue: Literal["steer", "follow_up"]
|
|
106
|
+
position: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CancelRequest(BaseModel):
|
|
110
|
+
"""POST /sessions/{id}/cancel body."""
|
|
111
|
+
|
|
112
|
+
request_id: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ToolConfirmationPOST(BaseModel):
|
|
116
|
+
"""POST /sessions/{id}/tool-confirmations/{tool_call_id} body."""
|
|
117
|
+
|
|
118
|
+
approved: bool
|
|
119
|
+
reason: str | None = None
|
|
120
|
+
always: bool = False
|
|
121
|
+
"""When true with approved, remember this tool+resource for the rest of the session."""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ElicitationPOST(BaseModel):
|
|
125
|
+
"""POST /sessions/{id}/elicitations/{id} body."""
|
|
126
|
+
|
|
127
|
+
user_data: Any | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FrontendToolResultPOST(BaseModel):
|
|
131
|
+
"""POST /sessions/{id}/frontend-tool-results/{tool_call_id} body."""
|
|
132
|
+
|
|
133
|
+
result: list[dict[str, Any]]
|
|
134
|
+
is_error: bool
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class HealthResponse(BaseModel):
|
|
138
|
+
"""GET /health response."""
|
|
139
|
+
|
|
140
|
+
status: Literal["ok"]
|
|
141
|
+
version: Literal["2.0.0"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SessionUsageResponse(BaseModel):
|
|
145
|
+
"""GET /sessions/{id}/usage response."""
|
|
146
|
+
|
|
147
|
+
session_id: str
|
|
148
|
+
turns: int
|
|
149
|
+
input_tokens: int
|
|
150
|
+
output_tokens: int
|
|
151
|
+
cached_tokens: int
|
|
152
|
+
cache_read_tokens: int = 0
|
|
153
|
+
cache_creation_tokens: int = 0
|
|
154
|
+
cost_usd: float
|
|
155
|
+
period_start: int
|
|
156
|
+
period_end: int
|
|
157
|
+
last_prompt_tokens: int = Field(
|
|
158
|
+
0,
|
|
159
|
+
description="Input (prompt) tokens reported for the most recent completed turn in this session.",
|
|
160
|
+
)
|
|
161
|
+
estimated_prompt_tokens: int = Field(
|
|
162
|
+
0,
|
|
163
|
+
description="Peak pre-stream prompt input token count for the latest turn (provider count_tokens / OpenAI tiktoken); aligns with summarization checks.",
|
|
164
|
+
)
|
|
165
|
+
summarization_threshold_tokens: int = Field(
|
|
166
|
+
0,
|
|
167
|
+
description="``floor(MODEL_CONTEXT_WINDOW * 0.85)`` — same bar as sync summarization in the agent loop.",
|
|
168
|
+
)
|
|
169
|
+
context_window_tokens: int = Field(
|
|
170
|
+
1_000_000,
|
|
171
|
+
description="Configured model context window cap used for UI fill estimates (see MODEL_CONTEXT_WINDOW).",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def error_payload_dict(code: str, message: str, request_id: str) -> dict[str, Any]:
|
|
176
|
+
"""Build a JSON-serializable error body for JSONResponse."""
|
|
177
|
+
return {"error": {"code": code, "message": message, "request_id": request_id}}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Normalize and validate POST /reply bodies into user ContentBlock lists."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from monkeybot.core.attachments.config import max_attachments_per_reply
|
|
6
|
+
from monkeybot.core.attachments.store import AttachmentStore
|
|
7
|
+
from monkeybot.core.types.content_blocks import (
|
|
8
|
+
AttachmentRef,
|
|
9
|
+
ContentBlock,
|
|
10
|
+
File,
|
|
11
|
+
Image,
|
|
12
|
+
Text,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReplyBodyError(ValueError):
|
|
17
|
+
"""Validation failure for reply body normalization."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, code: str, message: str) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.code = code
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_reply_to_user_content(
|
|
25
|
+
*,
|
|
26
|
+
message: str | None,
|
|
27
|
+
content: list[dict[str, object]] | None,
|
|
28
|
+
session_id: str,
|
|
29
|
+
attachment_store: AttachmentStore | None,
|
|
30
|
+
) -> list[ContentBlock]:
|
|
31
|
+
has_message = message is not None and message.strip() != ""
|
|
32
|
+
has_content = content is not None and len(content) > 0
|
|
33
|
+
if has_message and has_content:
|
|
34
|
+
raise ReplyBodyError("AMBIGUOUS_REPLY_BODY", "Send either message or content, not both")
|
|
35
|
+
if not has_message and not has_content:
|
|
36
|
+
raise ReplyBodyError("EMPTY_REPLY_BODY", "Reply must include message or content")
|
|
37
|
+
|
|
38
|
+
if has_message and not has_content:
|
|
39
|
+
return [Text(text=message or "")]
|
|
40
|
+
|
|
41
|
+
blocks: list[ContentBlock] = []
|
|
42
|
+
ref_count = 0
|
|
43
|
+
has_text = False
|
|
44
|
+
for raw in content or []:
|
|
45
|
+
if not isinstance(raw, dict):
|
|
46
|
+
raise ReplyBodyError("INVALID_CONTENT", "content blocks must be JSON objects")
|
|
47
|
+
tag = raw.get("type")
|
|
48
|
+
if tag in ("image", "file") and raw.get("data"):
|
|
49
|
+
raise ReplyBodyError(
|
|
50
|
+
"INLINE_ATTACHMENT_NOT_ALLOWED",
|
|
51
|
+
"Upload attachments via POST /attachments; use attachmentRef in content",
|
|
52
|
+
)
|
|
53
|
+
block = ContentBlock.from_dict(raw)
|
|
54
|
+
if isinstance(block, Image) or isinstance(block, File):
|
|
55
|
+
raise ReplyBodyError(
|
|
56
|
+
"INLINE_ATTACHMENT_NOT_ALLOWED",
|
|
57
|
+
"Upload attachments via POST /attachments; use attachmentRef in content",
|
|
58
|
+
)
|
|
59
|
+
if isinstance(block, Text):
|
|
60
|
+
if block.text.strip():
|
|
61
|
+
has_text = True
|
|
62
|
+
blocks.append(block)
|
|
63
|
+
elif isinstance(block, AttachmentRef):
|
|
64
|
+
ref_count += 1
|
|
65
|
+
if attachment_store is None or not attachment_store.exists(
|
|
66
|
+
session_id, block.attachment_id
|
|
67
|
+
):
|
|
68
|
+
raise ReplyBodyError(
|
|
69
|
+
"ATTACHMENT_NOT_FOUND",
|
|
70
|
+
f"Unknown attachment_id: {block.attachment_id}",
|
|
71
|
+
)
|
|
72
|
+
blocks.append(block)
|
|
73
|
+
else:
|
|
74
|
+
raise ReplyBodyError(
|
|
75
|
+
"INVALID_CONTENT",
|
|
76
|
+
f"Unsupported content block type for /reply: {type(block).__name__}",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if ref_count > max_attachments_per_reply():
|
|
80
|
+
raise ReplyBodyError(
|
|
81
|
+
"TOO_MANY_ATTACHMENTS",
|
|
82
|
+
f"At most {max_attachments_per_reply()} attachments per reply",
|
|
83
|
+
)
|
|
84
|
+
if ref_count == 0 and not has_text:
|
|
85
|
+
raise ReplyBodyError(
|
|
86
|
+
"EMPTY_REPLY_BODY",
|
|
87
|
+
"Reply content must include text or at least one attachmentRef",
|
|
88
|
+
)
|
|
89
|
+
if not blocks:
|
|
90
|
+
raise ReplyBodyError("EMPTY_REPLY_BODY", "Reply content is empty")
|
|
91
|
+
return blocks
|