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.
Files changed (144) hide show
  1. cli/__init__.py +0 -0
  2. cli/action_item_escalation.py +177 -0
  3. cli/agent_client.py +554 -0
  4. cli/bridge_state.py +43 -0
  5. cli/commands/__init__.py +13 -0
  6. cli/commands/action.py +142 -0
  7. cli/commands/agent.py +117 -0
  8. cli/commands/audit.py +68 -0
  9. cli/commands/docs.py +179 -0
  10. cli/commands/experiment.py +755 -0
  11. cli/commands/feedback.py +106 -0
  12. cli/commands/notification.py +213 -0
  13. cli/commands/persona.py +63 -0
  14. cli/commands/project.py +87 -0
  15. cli/commands/runtime.py +105 -0
  16. cli/commands/topic.py +361 -0
  17. cli/e2e_collab.py +602 -0
  18. cli/git_checkpoint.py +68 -0
  19. cli/host_worker_types.py +151 -0
  20. cli/main.py +1553 -0
  21. cli/map_command_client.py +497 -0
  22. cli/participant_worker.py +255 -0
  23. cli/reviewer_worker.py +263 -0
  24. cli/runtime/__init__.py +5 -0
  25. cli/runtime/run_lock.py +497 -0
  26. cli/runtime_chat.py +317 -0
  27. cli/session_wake_log.py +235 -0
  28. cli/simple_waker.py +950 -0
  29. cli/table_render.py +113 -0
  30. cli/wake_backend.py +236 -0
  31. cli/worker_cycle_log.py +36 -0
  32. map_client/__init__.py +37 -0
  33. map_client/bootstrap.py +193 -0
  34. map_client/client.py +1045 -0
  35. map_client/config.py +21 -0
  36. map_client/errors.py +283 -0
  37. map_client/exceptions.py +130 -0
  38. map_client/plan_evidence.py +159 -0
  39. map_client/project_config.py +153 -0
  40. map_client/result_template.py +167 -0
  41. map_client/testing.py +27 -0
  42. map_mcp/__init__.py +4 -0
  43. map_mcp/_utils.py +28 -0
  44. map_mcp/auth.py +34 -0
  45. map_mcp/config.py +50 -0
  46. map_mcp/context.py +39 -0
  47. map_mcp/main.py +75 -0
  48. map_mcp/server.py +573 -0
  49. map_mcp/session.py +79 -0
  50. map_sdk/__init__.py +29 -0
  51. map_sdk/evidence.py +68 -0
  52. map_types/__init__.py +203 -0
  53. map_types/enums.py +199 -0
  54. map_types/schemas.py +1351 -0
  55. multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
  56. multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
  57. multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
  58. multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
  59. multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
  60. multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
  61. server/__init__.py +0 -0
  62. server/__version__.py +14 -0
  63. server/api/__init__.py +0 -0
  64. server/api/action_items.py +138 -0
  65. server/api/agents.py +412 -0
  66. server/api/audit.py +54 -0
  67. server/api/background_tasks.py +18 -0
  68. server/api/common.py +117 -0
  69. server/api/deps.py +30 -0
  70. server/api/experiments.py +858 -0
  71. server/api/feedback.py +75 -0
  72. server/api/notifications.py +22 -0
  73. server/api/projects.py +209 -0
  74. server/api/router.py +25 -0
  75. server/api/status.py +33 -0
  76. server/api/topics.py +302 -0
  77. server/api/webhooks.py +74 -0
  78. server/auth/__init__.py +8 -0
  79. server/auth/experiment_access.py +66 -0
  80. server/config.py +38 -0
  81. server/db/__init__.py +3 -0
  82. server/db/base.py +5 -0
  83. server/db/deadlock_retry.py +146 -0
  84. server/db/session.py +41 -0
  85. server/domain/__init__.py +3 -0
  86. server/domain/encrypted_types.py +63 -0
  87. server/domain/models.py +713 -0
  88. server/domain/schemas.py +3 -0
  89. server/domain/state_machine.py +79 -0
  90. server/domain/topic_ack_constants.py +9 -0
  91. server/main.py +148 -0
  92. server/scripts/__init__.py +0 -0
  93. server/scripts/migrate_notification_unique.py +231 -0
  94. server/scripts/purge_audit_pollution.py +116 -0
  95. server/services/__init__.py +0 -0
  96. server/services/_lookups.py +26 -0
  97. server/services/acceptance_service.py +90 -0
  98. server/services/action_item_migration_service.py +190 -0
  99. server/services/action_item_service.py +200 -0
  100. server/services/agent_work_service.py +405 -0
  101. server/services/archive_lint_service.py +156 -0
  102. server/services/audit_service.py +457 -0
  103. server/services/auth.py +66 -0
  104. server/services/comment_service.py +173 -0
  105. server/services/errors.py +65 -0
  106. server/services/escalation_resolver.py +248 -0
  107. server/services/evidence_service.py +88 -0
  108. server/services/experiment_capabilities_service.py +277 -0
  109. server/services/inbound_event_service.py +111 -0
  110. server/services/lock_service.py +273 -0
  111. server/services/log_service.py +202 -0
  112. server/services/mention_service.py +730 -0
  113. server/services/notification_service.py +939 -0
  114. server/services/notification_stream.py +138 -0
  115. server/services/permissions.py +147 -0
  116. server/services/persona_activity_service.py +108 -0
  117. server/services/phase_owner_resolver.py +95 -0
  118. server/services/phase_service.py +381 -0
  119. server/services/plan_marker_service.py +235 -0
  120. server/services/plan_service.py +186 -0
  121. server/services/platform_feedback_service.py +114 -0
  122. server/services/project_service.py +534 -0
  123. server/services/project_status_service.py +132 -0
  124. server/services/review_service.py +707 -0
  125. server/services/secret_encryption.py +97 -0
  126. server/services/similarity_service.py +119 -0
  127. server/services/sse_event_schemas.py +17 -0
  128. server/services/status_service.py +68 -0
  129. server/services/template_service.py +134 -0
  130. server/services/text_utils.py +19 -0
  131. server/services/thread_activity.py +180 -0
  132. server/services/todo_persona_filter.py +73 -0
  133. server/services/todo_service.py +604 -0
  134. server/services/topic_ack_service.py +312 -0
  135. server/services/topic_action_item_ops.py +538 -0
  136. server/services/topic_comment_kind.py +14 -0
  137. server/services/topic_comment_service.py +237 -0
  138. server/services/topic_helpers.py +32 -0
  139. server/services/topic_lifecycle_service.py +478 -0
  140. server/services/topic_progress_service.py +40 -0
  141. server/services/topic_resolve_service.py +234 -0
  142. server/services/topic_service.py +102 -0
  143. server/services/topic_work_item_service.py +570 -0
  144. 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)