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,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)
|