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,186 @@
1
+ import uuid
2
+ from datetime import UTC, datetime
3
+
4
+ from map_types.enums import ReviewArchivedReason
5
+ from sqlalchemy import select
6
+ from sqlalchemy.orm import Session, joinedload
7
+
8
+ from server.domain.models import (
9
+ Agent,
10
+ AgentRole,
11
+ ExperimentPhase,
12
+ PlanVersion,
13
+ Review,
14
+ ReviewItem,
15
+ ReviewItemKind,
16
+ ReviewItemStatus,
17
+ )
18
+ from server.domain.schemas import PlanRevise
19
+ from server.domain.state_machine import ReviewItemTransitionContext, validate_review_item_transition
20
+ from server.services.errors import ForbiddenError, NotFoundError, StateTransitionError
21
+ from server.services.project_service import get_experiment
22
+
23
+
24
+ def list_plans(
25
+ db: Session,
26
+ experiment_id: uuid.UUID,
27
+ *,
28
+ limit: int = 50,
29
+ ) -> list[PlanVersion]:
30
+ get_experiment(db, experiment_id)
31
+ stmt = (
32
+ select(PlanVersion)
33
+ .where(PlanVersion.experiment_id == experiment_id)
34
+ .order_by(PlanVersion.version.asc())
35
+ .limit(max(1, min(limit, 200)))
36
+ )
37
+ return list(db.scalars(stmt))
38
+
39
+
40
+ def get_plan_version(db: Session, experiment_id: uuid.UUID, version: int) -> PlanVersion:
41
+ get_experiment(db, experiment_id)
42
+ stmt = select(PlanVersion).where(
43
+ PlanVersion.experiment_id == experiment_id,
44
+ PlanVersion.version == version,
45
+ )
46
+ plan = db.scalar(stmt)
47
+ if plan is None:
48
+ raise NotFoundError(f"Plan version {version} not found")
49
+ return plan
50
+
51
+
52
+ def _count_unclosed_unreasonable_items(db: Session, experiment_id: uuid.UUID) -> int:
53
+ stmt = (
54
+ select(ReviewItem)
55
+ .join(Review)
56
+ .where(
57
+ Review.experiment_id == experiment_id,
58
+ ReviewItem.kind == ReviewItemKind.unreasonable,
59
+ ReviewItem.status.in_(
60
+ (
61
+ ReviewItemStatus.open,
62
+ ReviewItemStatus.addressed,
63
+ ReviewItemStatus.rebutted,
64
+ ReviewItemStatus.escalated,
65
+ )
66
+ ),
67
+ )
68
+ )
69
+ return len(list(db.scalars(stmt)))
70
+
71
+
72
+ def _archive_prior_version_reviews(
73
+ db: Session,
74
+ *,
75
+ experiment_id: uuid.UUID,
76
+ new_version: int,
77
+ ) -> int:
78
+ """Mark every review whose plan_version < new_version as archived.
79
+
80
+ Called from ``revise_plan`` after ``experiment.current_plan_version``
81
+ is bumped. The cascade is idempotent: rows already carrying
82
+ ``archived_at`` are skipped so manual / superseded archives stay
83
+ untouched, and the trigger can safely run on repeat bumps without
84
+ overwriting audit metadata.
85
+
86
+ Returns the number of rows newly archived in this call (used by
87
+ end-to-end tests; service callers may ignore).
88
+ """
89
+ now = datetime.now(UTC).replace(tzinfo=None)
90
+ stmt = select(Review).where(
91
+ Review.experiment_id == experiment_id,
92
+ Review.plan_version < new_version,
93
+ Review.archived_at.is_(None),
94
+ )
95
+ rows = list(db.scalars(stmt))
96
+ for review in rows:
97
+ review.archived_at = now
98
+ review.archived_reason = ReviewArchivedReason.auto
99
+ return len(rows)
100
+
101
+
102
+ def revise_plan(
103
+ db: Session,
104
+ experiment_id: uuid.UUID,
105
+ author: Agent,
106
+ payload: PlanRevise,
107
+ ) -> PlanVersion:
108
+ experiment = get_experiment(db, experiment_id)
109
+ if experiment.creator_agent_id != author.id and author.role != AgentRole.admin:
110
+ raise ForbiddenError("Only the creator can revise the plan")
111
+ if experiment.phase not in (
112
+ ExperimentPhase.draft,
113
+ ExperimentPhase.review,
114
+ ExperimentPhase.running,
115
+ ):
116
+ raise StateTransitionError(
117
+ "Plan can only be revised in draft, review, or running phase"
118
+ )
119
+
120
+ # a764abf6 I1.(a): enforce plan frontmatter lint at revise time so
121
+ # missing required fields raise STATE_MACHINE_PLAN_MARKER_MISSING
122
+ # before any version bump / archive cascade runs.
123
+ from server.services.plan_marker_service import assert_plan_frontmatter_ok
124
+
125
+ assert_plan_frontmatter_ok(payload.content_md)
126
+
127
+ if not payload.addressed_item_ids and experiment.current_plan_version > 0:
128
+ current_plan = db.scalar(
129
+ select(PlanVersion).where(
130
+ PlanVersion.experiment_id == experiment.id,
131
+ PlanVersion.version == experiment.current_plan_version,
132
+ )
133
+ )
134
+ if (
135
+ current_plan is not None
136
+ and current_plan.content_md == payload.content_md
137
+ and _count_unclosed_unreasonable_items(db, experiment.id) == 0
138
+ ):
139
+ return current_plan
140
+
141
+ new_version = experiment.current_plan_version + 1
142
+ plan = PlanVersion(
143
+ experiment_id=experiment.id,
144
+ version=new_version,
145
+ content_md=payload.content_md,
146
+ author_agent_id=author.id,
147
+ change_note=payload.change_note,
148
+ )
149
+ db.add(plan)
150
+ experiment.current_plan_version = new_version
151
+
152
+ # I1(b) — auto-archive prior reviews: every review whose plan_version
153
+ # is less than the new current_plan_version is no longer canonical and
154
+ # should be marked archived_reason='auto' + archived_at=now().
155
+ # The early-return branch above (line 82-94) skips this on purpose:
156
+ # when content is unchanged and no items are addressed, no version bump
157
+ # happens so no archive cascade is appropriate.
158
+ _archive_prior_version_reviews(db, experiment_id=experiment.id, new_version=new_version)
159
+
160
+ if payload.addressed_item_ids:
161
+ items_stmt = (
162
+ select(ReviewItem)
163
+ .where(ReviewItem.id.in_(payload.addressed_item_ids))
164
+ .options(joinedload(ReviewItem.review))
165
+ )
166
+ items = list(db.scalars(items_stmt))
167
+ if len(items) != len(payload.addressed_item_ids):
168
+ raise NotFoundError("One or more review items not found")
169
+ for item in items:
170
+ if item.review.experiment_id != experiment.id:
171
+ raise ForbiddenError("Review item does not belong to this experiment")
172
+ if item.kind != ReviewItemKind.unreasonable:
173
+ raise StateTransitionError("Only unreasonable items can be addressed")
174
+ ctx = ReviewItemTransitionContext(
175
+ is_creator=True,
176
+ is_reviewer=False,
177
+ is_admin=author.role == AgentRole.admin,
178
+ via_plan_revision=True,
179
+ )
180
+ current = item.status or ReviewItemStatus.open
181
+ validate_review_item_transition(current, ReviewItemStatus.addressed, ctx)
182
+ item.status = ReviewItemStatus.addressed
183
+
184
+ db.commit()
185
+ db.refresh(plan)
186
+ return plan
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from datetime import UTC, datetime
5
+
6
+ from map_types.enums import FeedbackCategory, FeedbackStatus
7
+ from sqlalchemy import func, select
8
+ from sqlalchemy.orm import Session
9
+
10
+ from server.domain.models import Agent, PlatformFeedback
11
+ from server.domain.schemas import (
12
+ PlatformFeedbackCreate,
13
+ PlatformFeedbackRead,
14
+ PlatformFeedbackUpdate,
15
+ )
16
+ from server.services.errors import NotFoundError
17
+
18
+
19
+ def feedback_to_read(db: Session, feedback: PlatformFeedback) -> PlatformFeedbackRead:
20
+ author = db.get(Agent, feedback.author_agent_id)
21
+ return PlatformFeedbackRead(
22
+ id=feedback.id,
23
+ author_agent_id=feedback.author_agent_id,
24
+ author_name=author.name if author is not None else None,
25
+ project_id=feedback.project_id,
26
+ body=feedback.body,
27
+ category=feedback.category,
28
+ status=feedback.status,
29
+ metadata_json=feedback.metadata_json,
30
+ created_at=feedback.created_at,
31
+ updated_at=feedback.updated_at,
32
+ archived_at=feedback.archived_at,
33
+ )
34
+
35
+
36
+ def create_feedback(
37
+ db: Session,
38
+ agent: Agent,
39
+ payload: PlatformFeedbackCreate,
40
+ ) -> PlatformFeedbackRead:
41
+ # project_id is a source-context hint only; a project-bound agent defaults
42
+ # to its own project, admin may leave it global (None). NOT an access boundary.
43
+ project_id = payload.project_id if payload.project_id is not None else agent.project_id
44
+ feedback = PlatformFeedback(
45
+ author_agent_id=agent.id,
46
+ project_id=project_id,
47
+ body=payload.body,
48
+ category=payload.category,
49
+ status=FeedbackStatus.new,
50
+ metadata_json=payload.metadata,
51
+ )
52
+ db.add(feedback)
53
+ db.commit()
54
+ db.refresh(feedback)
55
+ return feedback_to_read(db, feedback)
56
+
57
+
58
+ def _get_feedback(db: Session, feedback_id: uuid.UUID) -> PlatformFeedback:
59
+ feedback = db.get(PlatformFeedback, feedback_id)
60
+ if feedback is None:
61
+ raise NotFoundError("Feedback not found")
62
+ return feedback
63
+
64
+
65
+ def get_feedback_read(db: Session, feedback_id: uuid.UUID) -> PlatformFeedbackRead:
66
+ return feedback_to_read(db, _get_feedback(db, feedback_id))
67
+
68
+
69
+ def list_feedback(
70
+ db: Session,
71
+ *,
72
+ status: FeedbackStatus | None = None,
73
+ category: FeedbackCategory | None = None,
74
+ project_id: uuid.UUID | None = None,
75
+ page: int = 1,
76
+ page_size: int = 50,
77
+ include_archived: bool = False,
78
+ ) -> tuple[list[PlatformFeedbackRead], int]:
79
+ stmt = select(PlatformFeedback)
80
+ if not include_archived:
81
+ stmt = stmt.where(PlatformFeedback.archived_at.is_(None))
82
+ if status is not None:
83
+ stmt = stmt.where(PlatformFeedback.status == status)
84
+ if category is not None:
85
+ stmt = stmt.where(PlatformFeedback.category == category)
86
+ if project_id is not None:
87
+ stmt = stmt.where(PlatformFeedback.project_id == project_id)
88
+ total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
89
+ page = max(1, page)
90
+ page_size = max(1, min(page_size, 200))
91
+ stmt = (
92
+ stmt.order_by(PlatformFeedback.created_at.desc())
93
+ .offset((page - 1) * page_size)
94
+ .limit(page_size)
95
+ )
96
+ rows = list(db.scalars(stmt))
97
+ return [feedback_to_read(db, f) for f in rows], total
98
+
99
+
100
+ def update_feedback(
101
+ db: Session,
102
+ feedback_id: uuid.UUID,
103
+ payload: PlatformFeedbackUpdate,
104
+ ) -> PlatformFeedbackRead:
105
+ feedback = _get_feedback(db, feedback_id)
106
+ data = payload.model_dump(exclude_unset=True)
107
+ archived = data.pop("archived", None)
108
+ for key, value in data.items():
109
+ setattr(feedback, key, value)
110
+ if archived is not None:
111
+ feedback.archived_at = datetime.now(UTC) if archived else None
112
+ db.commit()
113
+ db.refresh(feedback)
114
+ return feedback_to_read(db, feedback)