multi-agent-platform 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.
- cli/__init__.py +0 -0
- cli/action_item_escalation.py +177 -0
- cli/agent_client.py +554 -0
- cli/bridge_state.py +43 -0
- cli/commands/__init__.py +13 -0
- cli/commands/action.py +142 -0
- cli/commands/agent.py +117 -0
- cli/commands/audit.py +68 -0
- cli/commands/docs.py +179 -0
- cli/commands/experiment.py +755 -0
- cli/commands/feedback.py +106 -0
- cli/commands/notification.py +213 -0
- cli/commands/persona.py +63 -0
- cli/commands/project.py +87 -0
- cli/commands/runtime.py +105 -0
- cli/commands/topic.py +361 -0
- cli/e2e_collab.py +602 -0
- cli/git_checkpoint.py +68 -0
- cli/host_worker_types.py +151 -0
- cli/main.py +1553 -0
- cli/map_command_client.py +497 -0
- cli/participant_worker.py +255 -0
- cli/reviewer_worker.py +263 -0
- cli/runtime/__init__.py +5 -0
- cli/runtime/run_lock.py +497 -0
- cli/runtime_chat.py +317 -0
- cli/session_wake_log.py +235 -0
- cli/simple_waker.py +950 -0
- cli/table_render.py +113 -0
- cli/wake_backend.py +236 -0
- cli/worker_cycle_log.py +36 -0
- map_client/__init__.py +37 -0
- map_client/bootstrap.py +193 -0
- map_client/client.py +1045 -0
- map_client/config.py +21 -0
- map_client/errors.py +283 -0
- map_client/exceptions.py +130 -0
- map_client/plan_evidence.py +159 -0
- map_client/project_config.py +153 -0
- map_client/result_template.py +167 -0
- map_client/testing.py +27 -0
- map_mcp/__init__.py +4 -0
- map_mcp/_utils.py +28 -0
- map_mcp/auth.py +34 -0
- map_mcp/config.py +50 -0
- map_mcp/context.py +39 -0
- map_mcp/main.py +75 -0
- map_mcp/server.py +573 -0
- map_mcp/session.py +79 -0
- map_sdk/__init__.py +29 -0
- map_sdk/evidence.py +68 -0
- map_types/__init__.py +203 -0
- map_types/enums.py +199 -0
- map_types/schemas.py +1351 -0
- multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
- multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
- multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
- multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
- multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
- multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
- server/__init__.py +0 -0
- server/__version__.py +14 -0
- server/api/__init__.py +0 -0
- server/api/action_items.py +138 -0
- server/api/agents.py +412 -0
- server/api/audit.py +54 -0
- server/api/background_tasks.py +18 -0
- server/api/common.py +117 -0
- server/api/deps.py +30 -0
- server/api/experiments.py +858 -0
- server/api/feedback.py +75 -0
- server/api/notifications.py +22 -0
- server/api/projects.py +209 -0
- server/api/router.py +25 -0
- server/api/status.py +33 -0
- server/api/topics.py +302 -0
- server/api/webhooks.py +74 -0
- server/auth/__init__.py +8 -0
- server/auth/experiment_access.py +66 -0
- server/config.py +38 -0
- server/db/__init__.py +3 -0
- server/db/base.py +5 -0
- server/db/deadlock_retry.py +146 -0
- server/db/session.py +41 -0
- server/domain/__init__.py +3 -0
- server/domain/encrypted_types.py +63 -0
- server/domain/models.py +713 -0
- server/domain/schemas.py +3 -0
- server/domain/state_machine.py +79 -0
- server/domain/topic_ack_constants.py +9 -0
- server/main.py +148 -0
- server/scripts/__init__.py +0 -0
- server/scripts/migrate_notification_unique.py +231 -0
- server/scripts/purge_audit_pollution.py +116 -0
- server/services/__init__.py +0 -0
- server/services/_lookups.py +26 -0
- server/services/acceptance_service.py +90 -0
- server/services/action_item_migration_service.py +190 -0
- server/services/action_item_service.py +200 -0
- server/services/agent_work_service.py +405 -0
- server/services/archive_lint_service.py +156 -0
- server/services/audit_service.py +457 -0
- server/services/auth.py +66 -0
- server/services/comment_service.py +173 -0
- server/services/errors.py +65 -0
- server/services/escalation_resolver.py +248 -0
- server/services/evidence_service.py +88 -0
- server/services/experiment_capabilities_service.py +277 -0
- server/services/inbound_event_service.py +111 -0
- server/services/lock_service.py +273 -0
- server/services/log_service.py +202 -0
- server/services/mention_service.py +730 -0
- server/services/notification_service.py +939 -0
- server/services/notification_stream.py +138 -0
- server/services/permissions.py +147 -0
- server/services/persona_activity_service.py +108 -0
- server/services/phase_owner_resolver.py +95 -0
- server/services/phase_service.py +381 -0
- server/services/plan_marker_service.py +235 -0
- server/services/plan_service.py +186 -0
- server/services/platform_feedback_service.py +114 -0
- server/services/project_service.py +534 -0
- server/services/project_status_service.py +132 -0
- server/services/review_service.py +707 -0
- server/services/secret_encryption.py +97 -0
- server/services/similarity_service.py +119 -0
- server/services/sse_event_schemas.py +17 -0
- server/services/status_service.py +68 -0
- server/services/template_service.py +134 -0
- server/services/text_utils.py +19 -0
- server/services/thread_activity.py +180 -0
- server/services/todo_persona_filter.py +73 -0
- server/services/todo_service.py +604 -0
- server/services/topic_ack_service.py +312 -0
- server/services/topic_action_item_ops.py +538 -0
- server/services/topic_comment_kind.py +14 -0
- server/services/topic_comment_service.py +237 -0
- server/services/topic_helpers.py +32 -0
- server/services/topic_lifecycle_service.py +478 -0
- server/services/topic_progress_service.py +40 -0
- server/services/topic_resolve_service.py +234 -0
- server/services/topic_service.py +102 -0
- server/services/topic_work_item_service.py +570 -0
- server/services/webhook_service.py +273 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import secrets
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from sqlalchemy import delete, or_, select
|
|
14
|
+
from sqlalchemy.orm import Session, joinedload
|
|
15
|
+
|
|
16
|
+
from server.db import session as db_session_module
|
|
17
|
+
from server.domain.models import Webhook, WebhookDelivery
|
|
18
|
+
from server.domain.schemas import WebhookCreate, WebhookUpdate
|
|
19
|
+
from server.services.errors import NotFoundError
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# 单次投递最多尝试次数(含首次)。重试间隔为 2 ** (attempt-1) 秒,即 1s / 2s。
|
|
24
|
+
# 总最坏阻塞时长 ≈ 1 + 2 = 3s,仍短于 httpx 单次 5s timeout,可控。
|
|
25
|
+
DEFAULT_MAX_ATTEMPTS = 3
|
|
26
|
+
DEFAULT_BACKOFF_BASE = 1.0 # seconds
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _http_post(url: str, body: bytes, signature: str) -> int:
|
|
30
|
+
"""Send a webhook payload. Overridable in tests via monkeypatch."""
|
|
31
|
+
resp = httpx.post(
|
|
32
|
+
url,
|
|
33
|
+
content=body,
|
|
34
|
+
headers={
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"X-MAP-Signature": signature,
|
|
37
|
+
},
|
|
38
|
+
timeout=5.0,
|
|
39
|
+
)
|
|
40
|
+
return resp.status_code
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_webhook(db: Session, payload: WebhookCreate) -> tuple[Webhook, str]:
|
|
44
|
+
secret = secrets.token_urlsafe(32)
|
|
45
|
+
webhook = Webhook(
|
|
46
|
+
project_id=payload.project_id,
|
|
47
|
+
url=payload.url,
|
|
48
|
+
events=payload.events,
|
|
49
|
+
secret=secret,
|
|
50
|
+
active=True,
|
|
51
|
+
)
|
|
52
|
+
db.add(webhook)
|
|
53
|
+
db.commit()
|
|
54
|
+
db.refresh(webhook)
|
|
55
|
+
return webhook, secret
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def list_webhooks(db: Session, project_id: uuid.UUID | None = None) -> list[Webhook]:
|
|
59
|
+
stmt = select(Webhook).order_by(Webhook.created_at.desc())
|
|
60
|
+
if project_id is not None:
|
|
61
|
+
stmt = stmt.where((Webhook.project_id == project_id) | (Webhook.project_id.is_(None)))
|
|
62
|
+
return list(db.scalars(stmt))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_webhook(db: Session, webhook_id: uuid.UUID) -> Webhook:
|
|
66
|
+
webhook = db.get(Webhook, webhook_id)
|
|
67
|
+
if webhook is None:
|
|
68
|
+
raise NotFoundError("Webhook not found")
|
|
69
|
+
return webhook
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def update_webhook(db: Session, webhook_id: uuid.UUID, payload: WebhookUpdate) -> Webhook:
|
|
73
|
+
webhook = get_webhook(db, webhook_id)
|
|
74
|
+
for key, value in payload.model_dump(exclude_unset=True).items():
|
|
75
|
+
setattr(webhook, key, value)
|
|
76
|
+
db.commit()
|
|
77
|
+
db.refresh(webhook)
|
|
78
|
+
return webhook
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def delete_webhook(db: Session, webhook_id: uuid.UUID) -> None:
|
|
82
|
+
webhook = get_webhook(db, webhook_id)
|
|
83
|
+
db.execute(delete(WebhookDelivery).where(WebhookDelivery.webhook_id == webhook_id))
|
|
84
|
+
db.delete(webhook)
|
|
85
|
+
db.commit()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def list_deliveries(db: Session, webhook_id: uuid.UUID) -> list[WebhookDelivery]:
|
|
89
|
+
get_webhook(db, webhook_id)
|
|
90
|
+
stmt = (
|
|
91
|
+
select(WebhookDelivery)
|
|
92
|
+
.where(WebhookDelivery.webhook_id == webhook_id)
|
|
93
|
+
.order_by(WebhookDelivery.created_at.desc())
|
|
94
|
+
)
|
|
95
|
+
return list(db.scalars(stmt))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _matching_webhooks(db: Session, project_id: uuid.UUID | None, event: str) -> list[Webhook]:
|
|
99
|
+
stmt = select(Webhook).where(Webhook.active.is_(True))
|
|
100
|
+
if project_id is not None:
|
|
101
|
+
stmt = stmt.where(or_(Webhook.project_id == project_id, Webhook.project_id.is_(None)))
|
|
102
|
+
matched: list[Webhook] = []
|
|
103
|
+
for wh in db.scalars(stmt):
|
|
104
|
+
if wh.events and event not in wh.events:
|
|
105
|
+
continue
|
|
106
|
+
matched.append(wh)
|
|
107
|
+
return matched
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def enqueue_event_deliveries(
|
|
111
|
+
db: Session,
|
|
112
|
+
event: str,
|
|
113
|
+
payload: dict,
|
|
114
|
+
project_id: uuid.UUID | None,
|
|
115
|
+
) -> list[uuid.UUID]:
|
|
116
|
+
"""Create pending delivery rows; HTTP happens in deliver_delivery()."""
|
|
117
|
+
body_obj = {"event": event, "payload": payload}
|
|
118
|
+
delivery_ids: list[uuid.UUID] = []
|
|
119
|
+
for webhook in _matching_webhooks(db, project_id, event):
|
|
120
|
+
delivery = WebhookDelivery(
|
|
121
|
+
webhook_id=webhook.id,
|
|
122
|
+
event=event,
|
|
123
|
+
payload=body_obj,
|
|
124
|
+
attempts=0,
|
|
125
|
+
success=False,
|
|
126
|
+
)
|
|
127
|
+
db.add(delivery)
|
|
128
|
+
db.flush()
|
|
129
|
+
delivery_ids.append(delivery.id)
|
|
130
|
+
if delivery_ids:
|
|
131
|
+
db.commit()
|
|
132
|
+
return delivery_ids
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _summarize_error(status_code: int | None, exc: BaseException | None) -> str | None:
|
|
136
|
+
"""Compact one-line error summary for ``last_error``."""
|
|
137
|
+
if exc is not None:
|
|
138
|
+
return f"{type(exc).__name__}: {exc}"[:500]
|
|
139
|
+
if status_code is None:
|
|
140
|
+
return None
|
|
141
|
+
if 200 <= status_code < 300:
|
|
142
|
+
return None
|
|
143
|
+
return f"HTTP {status_code}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _perform_delivery(
|
|
147
|
+
db: Session,
|
|
148
|
+
delivery_id: uuid.UUID,
|
|
149
|
+
*,
|
|
150
|
+
max_attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
151
|
+
backoff_base: float = DEFAULT_BACKOFF_BASE,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Deliver one payload with bounded retry.
|
|
154
|
+
|
|
155
|
+
Strategy:
|
|
156
|
+
- 2xx → success, stop.
|
|
157
|
+
- 4xx (except 408 / 429) → client error, do not retry (per common webhook
|
|
158
|
+
convention; the receiver explicitly rejected the payload).
|
|
159
|
+
- Network error / 408 / 429 / 5xx → retry with exponential backoff
|
|
160
|
+
(``backoff_base * 2 ** (attempt-1)``), up to ``max_attempts``.
|
|
161
|
+
|
|
162
|
+
Each attempt updates ``attempts`` / ``last_attempt_at`` / ``status_code`` /
|
|
163
|
+
``last_error`` so admin can inspect progress in the deliveries list. The
|
|
164
|
+
function is sync because ``deliver_delivery`` runs in a BackgroundTask and
|
|
165
|
+
we accept the small worst-case latency (~3 retries × 5s timeout) for the
|
|
166
|
+
sake of reliable delivery.
|
|
167
|
+
"""
|
|
168
|
+
delivery = db.scalar(
|
|
169
|
+
select(WebhookDelivery)
|
|
170
|
+
.where(WebhookDelivery.id == delivery_id)
|
|
171
|
+
.options(joinedload(WebhookDelivery.webhook))
|
|
172
|
+
)
|
|
173
|
+
if delivery is None or delivery.webhook is None:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
webhook = delivery.webhook
|
|
177
|
+
body = json.dumps(delivery.payload, default=str, ensure_ascii=False).encode("utf-8")
|
|
178
|
+
signature = (
|
|
179
|
+
"sha256="
|
|
180
|
+
+ hmac.new(webhook.secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
last_status: int | None = None
|
|
184
|
+
last_exc: BaseException | None = None
|
|
185
|
+
success = False
|
|
186
|
+
attempts = 0
|
|
187
|
+
|
|
188
|
+
# Retry only on network errors and transient status codes (timeout / too many
|
|
189
|
+
# requests / server errors). 4xx (except 408/429) means the receiver rejected
|
|
190
|
+
# the payload, so retrying won't help.
|
|
191
|
+
transient_statuses = {408, 425, 429, 500, 502, 503, 504}
|
|
192
|
+
|
|
193
|
+
for attempt in range(1, max_attempts + 1):
|
|
194
|
+
attempts = attempt
|
|
195
|
+
last_exc = None
|
|
196
|
+
try:
|
|
197
|
+
last_status = _http_post(webhook.url, body, signature)
|
|
198
|
+
delivery.status_code = last_status
|
|
199
|
+
if 200 <= last_status < 300:
|
|
200
|
+
success = True
|
|
201
|
+
break
|
|
202
|
+
if last_status not in transient_statuses:
|
|
203
|
+
# 4xx (non-transient): client error, no point retrying.
|
|
204
|
+
break
|
|
205
|
+
except Exception as exc: # noqa: BLE001 — external HTTP, broad catch intended
|
|
206
|
+
last_exc = exc
|
|
207
|
+
last_status = None
|
|
208
|
+
delivery.status_code = None
|
|
209
|
+
logger.warning(
|
|
210
|
+
"webhook delivery %s attempt %d/%d failed: %s",
|
|
211
|
+
delivery_id,
|
|
212
|
+
attempt,
|
|
213
|
+
max_attempts,
|
|
214
|
+
exc,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Persist progress so admin can watch attempts tick up even mid-retry.
|
|
218
|
+
delivery.attempts = attempts
|
|
219
|
+
delivery.last_attempt_at = datetime.now(UTC)
|
|
220
|
+
delivery.last_error = _summarize_error(last_status, last_exc)
|
|
221
|
+
db.flush()
|
|
222
|
+
|
|
223
|
+
if attempt < max_attempts:
|
|
224
|
+
sleep_for = backoff_base * (2 ** (attempt - 1))
|
|
225
|
+
time.sleep(sleep_for)
|
|
226
|
+
|
|
227
|
+
delivery.attempts = attempts
|
|
228
|
+
delivery.success = success
|
|
229
|
+
delivery.last_attempt_at = datetime.now(UTC)
|
|
230
|
+
delivery.last_error = _summarize_error(last_status, last_exc)
|
|
231
|
+
if success:
|
|
232
|
+
logger.info(
|
|
233
|
+
"webhook delivery %s succeeded after %d attempt(s)",
|
|
234
|
+
delivery_id,
|
|
235
|
+
attempts,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
logger.warning(
|
|
239
|
+
"webhook delivery %s failed after %d attempt(s): %s",
|
|
240
|
+
delivery_id,
|
|
241
|
+
attempts,
|
|
242
|
+
delivery.last_error,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def deliver_deliveries(db: Session, delivery_ids: list[uuid.UUID]) -> None:
|
|
247
|
+
"""Deliver using the caller's DB session (sync fallback / tests)."""
|
|
248
|
+
if not delivery_ids:
|
|
249
|
+
return
|
|
250
|
+
for delivery_id in delivery_ids:
|
|
251
|
+
_perform_delivery(db, delivery_id)
|
|
252
|
+
db.commit()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def deliver_delivery(delivery_id: uuid.UUID) -> None:
|
|
256
|
+
"""Perform HTTP delivery in a background task (uses its own DB session)."""
|
|
257
|
+
db = db_session_module.SessionLocal()
|
|
258
|
+
try:
|
|
259
|
+
_perform_delivery(db, delivery_id)
|
|
260
|
+
db.commit()
|
|
261
|
+
finally:
|
|
262
|
+
db.close()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def deliver_event(
|
|
266
|
+
db: Session,
|
|
267
|
+
event: str,
|
|
268
|
+
payload: dict,
|
|
269
|
+
project_id: uuid.UUID | None,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Synchronous delivery (tests / fallback when no BackgroundTasks bound)."""
|
|
272
|
+
delivery_ids = enqueue_event_deliveries(db, event, payload, project_id)
|
|
273
|
+
deliver_deliveries(db, delivery_ids)
|