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
server/api/feedback.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, Query, Response, status
|
|
4
|
+
from map_types.enums import FeedbackCategory, FeedbackStatus
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from server.api.deps import get_current_agent
|
|
8
|
+
from server.db.session import get_db
|
|
9
|
+
from server.domain.models import Agent
|
|
10
|
+
from server.domain.schemas import (
|
|
11
|
+
PlatformFeedbackCreate,
|
|
12
|
+
PlatformFeedbackRead,
|
|
13
|
+
PlatformFeedbackUpdate,
|
|
14
|
+
)
|
|
15
|
+
from server.services import permissions as perm
|
|
16
|
+
from server.services import platform_feedback_service as svc
|
|
17
|
+
|
|
18
|
+
feedback_router = APIRouter(prefix="/feedback", tags=["feedback"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@feedback_router.post("", response_model=PlatformFeedbackRead, status_code=status.HTTP_201_CREATED)
|
|
22
|
+
def submit_feedback(
|
|
23
|
+
payload: PlatformFeedbackCreate,
|
|
24
|
+
db: Session = Depends(get_db),
|
|
25
|
+
agent: Agent = Depends(get_current_agent),
|
|
26
|
+
) -> PlatformFeedbackRead:
|
|
27
|
+
"""Any authenticated agent may submit feedback. No project boundary enforced."""
|
|
28
|
+
return svc.create_feedback(db, agent, payload)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@feedback_router.get("", response_model=list[PlatformFeedbackRead])
|
|
32
|
+
def list_feedback(
|
|
33
|
+
response: Response,
|
|
34
|
+
feedback_status: FeedbackStatus | None = Query(default=None, alias="status"),
|
|
35
|
+
category: FeedbackCategory | None = Query(default=None),
|
|
36
|
+
project_id: uuid.UUID | None = Query(default=None),
|
|
37
|
+
page: int = Query(default=1, ge=1),
|
|
38
|
+
page_size: int = Query(default=50, ge=1, le=200),
|
|
39
|
+
include_archived: bool = Query(default=False),
|
|
40
|
+
db: Session = Depends(get_db),
|
|
41
|
+
agent: Agent = Depends(get_current_agent),
|
|
42
|
+
) -> list[PlatformFeedbackRead]:
|
|
43
|
+
perm.require_admin(agent)
|
|
44
|
+
items, total = svc.list_feedback(
|
|
45
|
+
db,
|
|
46
|
+
status=feedback_status,
|
|
47
|
+
category=category,
|
|
48
|
+
project_id=project_id,
|
|
49
|
+
page=page,
|
|
50
|
+
page_size=page_size,
|
|
51
|
+
include_archived=include_archived,
|
|
52
|
+
)
|
|
53
|
+
response.headers["X-Total-Count"] = str(total)
|
|
54
|
+
return items
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@feedback_router.get("/{feedback_id}", response_model=PlatformFeedbackRead)
|
|
58
|
+
def get_feedback(
|
|
59
|
+
feedback_id: uuid.UUID,
|
|
60
|
+
db: Session = Depends(get_db),
|
|
61
|
+
agent: Agent = Depends(get_current_agent),
|
|
62
|
+
) -> PlatformFeedbackRead:
|
|
63
|
+
perm.require_admin(agent)
|
|
64
|
+
return svc.get_feedback_read(db, feedback_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@feedback_router.patch("/{feedback_id}", response_model=PlatformFeedbackRead)
|
|
68
|
+
def update_feedback(
|
|
69
|
+
feedback_id: uuid.UUID,
|
|
70
|
+
payload: PlatformFeedbackUpdate,
|
|
71
|
+
db: Session = Depends(get_db),
|
|
72
|
+
agent: Agent = Depends(get_current_agent),
|
|
73
|
+
) -> PlatformFeedbackRead:
|
|
74
|
+
perm.require_admin(agent)
|
|
75
|
+
return svc.update_feedback(db, feedback_id, payload)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from server.api.deps import get_current_agent
|
|
7
|
+
from server.db.session import get_db
|
|
8
|
+
from server.domain.models import Agent
|
|
9
|
+
from server.domain.schemas import NotificationRead
|
|
10
|
+
from server.services import notification_service
|
|
11
|
+
|
|
12
|
+
notifications_router = APIRouter(prefix="/notifications", tags=["notifications"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@notifications_router.post("/{notification_id}/read", response_model=NotificationRead)
|
|
16
|
+
def mark_notification_read(
|
|
17
|
+
notification_id: uuid.UUID,
|
|
18
|
+
agent: Agent = Depends(get_current_agent),
|
|
19
|
+
db: Session = Depends(get_db),
|
|
20
|
+
) -> NotificationRead:
|
|
21
|
+
notification = notification_service.mark_read(db, agent, notification_id)
|
|
22
|
+
return NotificationRead.model_validate(notification)
|
server/api/projects.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, Query, status
|
|
4
|
+
from map_types.enums import TopicActionItemStatus
|
|
5
|
+
from sqlalchemy import or_, select
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
|
|
8
|
+
from server.api.background_tasks import bind_background_tasks
|
|
9
|
+
from server.api.common import emit
|
|
10
|
+
from server.api.deps import get_current_agent
|
|
11
|
+
from server.db.session import get_db
|
|
12
|
+
from server.domain.models import Agent, AgentRole
|
|
13
|
+
from server.domain.schemas import (
|
|
14
|
+
AgentRead,
|
|
15
|
+
ProjectCreate,
|
|
16
|
+
ProjectRead,
|
|
17
|
+
ProjectStatusRead,
|
|
18
|
+
ProjectStatusRevise,
|
|
19
|
+
ProjectStatusVersionRead,
|
|
20
|
+
ProjectUpdate,
|
|
21
|
+
TopicActionItemRead,
|
|
22
|
+
TopicDecisionRead,
|
|
23
|
+
)
|
|
24
|
+
from server.services import permissions as perm
|
|
25
|
+
from server.services import project_service as svc
|
|
26
|
+
from server.services import project_status_service as status_doc_service
|
|
27
|
+
|
|
28
|
+
router = APIRouter(
|
|
29
|
+
prefix="/projects",
|
|
30
|
+
tags=["projects"],
|
|
31
|
+
dependencies=[Depends(bind_background_tasks)],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("", response_model=ProjectRead, status_code=status.HTTP_201_CREATED)
|
|
36
|
+
def create_project(
|
|
37
|
+
payload: ProjectCreate,
|
|
38
|
+
db: Session = Depends(get_db),
|
|
39
|
+
agent: Agent = Depends(get_current_agent),
|
|
40
|
+
) -> ProjectRead:
|
|
41
|
+
perm.require_admin(agent)
|
|
42
|
+
project = svc.create_project(db, payload, author_agent_id=agent.id)
|
|
43
|
+
return ProjectRead.model_validate(project)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.get("", response_model=list[ProjectRead])
|
|
47
|
+
def list_projects(
|
|
48
|
+
include_archived: bool = Query(default=False),
|
|
49
|
+
db: Session = Depends(get_db),
|
|
50
|
+
agent: Agent = Depends(get_current_agent),
|
|
51
|
+
) -> list[ProjectRead]:
|
|
52
|
+
if perm.is_admin(agent):
|
|
53
|
+
projects = svc.list_projects(db, include_archived=include_archived)
|
|
54
|
+
elif agent.project_id is None:
|
|
55
|
+
projects = []
|
|
56
|
+
else:
|
|
57
|
+
projects = svc.list_projects(
|
|
58
|
+
db, include_archived=include_archived, project_id=agent.project_id
|
|
59
|
+
)
|
|
60
|
+
return [ProjectRead.model_validate(p) for p in projects]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.get("/by-key/{project_key}", response_model=ProjectRead)
|
|
64
|
+
def get_project_by_key(
|
|
65
|
+
project_key: str,
|
|
66
|
+
db: Session = Depends(get_db),
|
|
67
|
+
agent: Agent = Depends(get_current_agent),
|
|
68
|
+
) -> ProjectRead:
|
|
69
|
+
project = svc.get_project_by_key(db, project_key)
|
|
70
|
+
perm.ensure_project_access(agent, project.id)
|
|
71
|
+
return ProjectRead.model_validate(project)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/{project_id}", response_model=ProjectRead)
|
|
75
|
+
def get_project(
|
|
76
|
+
project_id: uuid.UUID,
|
|
77
|
+
db: Session = Depends(get_db),
|
|
78
|
+
agent: Agent = Depends(get_current_agent),
|
|
79
|
+
) -> ProjectRead:
|
|
80
|
+
perm.ensure_project_access(agent, project_id)
|
|
81
|
+
project = svc.get_project(db, project_id)
|
|
82
|
+
return ProjectRead.model_validate(project)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.get("/{project_id}/agents", response_model=list[AgentRead])
|
|
86
|
+
def list_project_agents(
|
|
87
|
+
project_id: uuid.UUID,
|
|
88
|
+
db: Session = Depends(get_db),
|
|
89
|
+
agent: Agent = Depends(get_current_agent),
|
|
90
|
+
) -> list[AgentRead]:
|
|
91
|
+
perm.ensure_project_access(agent, project_id)
|
|
92
|
+
svc.get_project(db, project_id)
|
|
93
|
+
agents = list(
|
|
94
|
+
db.scalars(
|
|
95
|
+
select(Agent)
|
|
96
|
+
.where(or_(Agent.project_id == project_id, Agent.role == AgentRole.admin))
|
|
97
|
+
.order_by(Agent.name)
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return [
|
|
101
|
+
AgentRead(
|
|
102
|
+
id=a.id,
|
|
103
|
+
name=a.name,
|
|
104
|
+
role=a.role,
|
|
105
|
+
project_id=a.project_id,
|
|
106
|
+
project_key=None,
|
|
107
|
+
created_at=a.created_at,
|
|
108
|
+
)
|
|
109
|
+
for a in agents
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@router.patch("/{project_id}", response_model=ProjectRead)
|
|
114
|
+
def update_project(
|
|
115
|
+
project_id: uuid.UUID,
|
|
116
|
+
payload: ProjectUpdate,
|
|
117
|
+
db: Session = Depends(get_db),
|
|
118
|
+
agent: Agent = Depends(get_current_agent),
|
|
119
|
+
) -> ProjectRead:
|
|
120
|
+
perm.require_admin(agent)
|
|
121
|
+
project = svc.update_project(db, project_id, payload)
|
|
122
|
+
return ProjectRead.model_validate(project)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@router.get("/{project_id}/status", response_model=ProjectStatusRead)
|
|
126
|
+
def get_project_status(
|
|
127
|
+
project_id: uuid.UUID,
|
|
128
|
+
db: Session = Depends(get_db),
|
|
129
|
+
agent: Agent = Depends(get_current_agent),
|
|
130
|
+
) -> ProjectStatusRead:
|
|
131
|
+
perm.ensure_project_access(agent, project_id)
|
|
132
|
+
return svc.get_project_status(db, project_id)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.get("/{project_id}/decisions", response_model=list[TopicDecisionRead])
|
|
136
|
+
def list_project_decisions(
|
|
137
|
+
project_id: uuid.UUID,
|
|
138
|
+
limit: int = Query(default=20, ge=1, le=100),
|
|
139
|
+
db: Session = Depends(get_db),
|
|
140
|
+
agent: Agent = Depends(get_current_agent),
|
|
141
|
+
) -> list[TopicDecisionRead]:
|
|
142
|
+
perm.ensure_project_access(agent, project_id)
|
|
143
|
+
return svc.list_project_decisions(db, project_id, limit=limit)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.get("/{project_id}/action-items", response_model=list[TopicActionItemRead])
|
|
147
|
+
def list_project_action_items(
|
|
148
|
+
project_id: uuid.UUID,
|
|
149
|
+
owner_agent_id: uuid.UUID | None = Query(default=None),
|
|
150
|
+
item_status: TopicActionItemStatus | None = Query(default=None, alias="status"),
|
|
151
|
+
limit: int = Query(default=100, ge=1, le=200),
|
|
152
|
+
db: Session = Depends(get_db),
|
|
153
|
+
agent: Agent = Depends(get_current_agent),
|
|
154
|
+
) -> list[TopicActionItemRead]:
|
|
155
|
+
perm.ensure_project_access(agent, project_id)
|
|
156
|
+
return svc.list_project_action_items(
|
|
157
|
+
db,
|
|
158
|
+
project_id,
|
|
159
|
+
owner_agent_id=owner_agent_id,
|
|
160
|
+
status=item_status,
|
|
161
|
+
limit=limit,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@router.post(
|
|
166
|
+
"/{project_id}/status/revisions",
|
|
167
|
+
response_model=ProjectStatusVersionRead,
|
|
168
|
+
status_code=status.HTTP_201_CREATED,
|
|
169
|
+
)
|
|
170
|
+
def revise_project_status(
|
|
171
|
+
project_id: uuid.UUID,
|
|
172
|
+
payload: ProjectStatusRevise,
|
|
173
|
+
db: Session = Depends(get_db),
|
|
174
|
+
agent: Agent = Depends(get_current_agent),
|
|
175
|
+
) -> ProjectStatusVersionRead:
|
|
176
|
+
perm.ensure_can_revise_project_status(agent, project_id)
|
|
177
|
+
version = status_doc_service.revise_status(db, project_id, agent.id, payload)
|
|
178
|
+
emit(
|
|
179
|
+
db,
|
|
180
|
+
agent,
|
|
181
|
+
action="project_status.revised",
|
|
182
|
+
target_type="project_status_version",
|
|
183
|
+
target_id=version.id,
|
|
184
|
+
project_id=project_id,
|
|
185
|
+
summary=f"修订项目 Status v{version.version}",
|
|
186
|
+
)
|
|
187
|
+
return ProjectStatusVersionRead.model_validate(version)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.get("/{project_id}/status/versions", response_model=list[ProjectStatusVersionRead])
|
|
191
|
+
def list_project_status_versions(
|
|
192
|
+
project_id: uuid.UUID,
|
|
193
|
+
limit: int = Query(default=50, ge=1, le=200),
|
|
194
|
+
db: Session = Depends(get_db),
|
|
195
|
+
agent: Agent = Depends(get_current_agent),
|
|
196
|
+
) -> list[ProjectStatusVersionRead]:
|
|
197
|
+
perm.ensure_project_access(agent, project_id)
|
|
198
|
+
return status_doc_service.list_status_versions(db, project_id, limit=limit)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@router.get("/{project_id}/status/versions/{version}", response_model=ProjectStatusVersionRead)
|
|
202
|
+
def get_project_status_version(
|
|
203
|
+
project_id: uuid.UUID,
|
|
204
|
+
version: int,
|
|
205
|
+
db: Session = Depends(get_db),
|
|
206
|
+
agent: Agent = Depends(get_current_agent),
|
|
207
|
+
) -> ProjectStatusVersionRead:
|
|
208
|
+
perm.ensure_project_access(agent, project_id)
|
|
209
|
+
return status_doc_service.get_status_version(db, project_id, version)
|
server/api/router.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Aggregate API routers for backward compatibility."""
|
|
2
|
+
|
|
3
|
+
from server.api.action_items import action_items_router
|
|
4
|
+
from server.api.agents import agents_router
|
|
5
|
+
from server.api.audit import audit_router
|
|
6
|
+
from server.api.experiments import experiments_router
|
|
7
|
+
from server.api.feedback import feedback_router
|
|
8
|
+
from server.api.notifications import notifications_router
|
|
9
|
+
from server.api.projects import router
|
|
10
|
+
from server.api.status import status_router
|
|
11
|
+
from server.api.topics import topics_router
|
|
12
|
+
from server.api.webhooks import webhooks_router
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"action_items_router",
|
|
16
|
+
"notifications_router",
|
|
17
|
+
"router",
|
|
18
|
+
"agents_router",
|
|
19
|
+
"audit_router",
|
|
20
|
+
"experiments_router",
|
|
21
|
+
"feedback_router",
|
|
22
|
+
"status_router",
|
|
23
|
+
"topics_router",
|
|
24
|
+
"webhooks_router",
|
|
25
|
+
]
|
server/api/status.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, Query
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from server.api.deps import get_current_agent
|
|
7
|
+
from server.db.session import get_db
|
|
8
|
+
from server.domain.models import Agent
|
|
9
|
+
from server.domain.schemas import (
|
|
10
|
+
GlobalStatusRead,
|
|
11
|
+
)
|
|
12
|
+
from server.services import permissions as perm
|
|
13
|
+
from server.services import status_service
|
|
14
|
+
from server.services.errors import ForbiddenError
|
|
15
|
+
|
|
16
|
+
status_router = APIRouter(prefix="/status", tags=["status"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@status_router.get("", response_model=GlobalStatusRead)
|
|
20
|
+
def get_global_status(
|
|
21
|
+
project_id: uuid.UUID | None = Query(default=None),
|
|
22
|
+
db: Session = Depends(get_db),
|
|
23
|
+
agent: Agent = Depends(get_current_agent),
|
|
24
|
+
) -> GlobalStatusRead:
|
|
25
|
+
if project_id is None:
|
|
26
|
+
if not perm.is_admin(agent):
|
|
27
|
+
if agent.project_id is None:
|
|
28
|
+
raise ForbiddenError("Agent is not bound to a project")
|
|
29
|
+
project_id = agent.project_id
|
|
30
|
+
else:
|
|
31
|
+
return status_service.get_global_status(db, project_id=None)
|
|
32
|
+
perm.ensure_project_access(agent, project_id)
|
|
33
|
+
return status_service.get_global_status(db, project_id=project_id)
|
server/api/topics.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from server.api.background_tasks import bind_background_tasks
|
|
7
|
+
from server.api.common import emit
|
|
8
|
+
from server.api.deps import get_current_agent
|
|
9
|
+
from server.db.session import get_db
|
|
10
|
+
from server.domain.models import Agent, TopicStatus
|
|
11
|
+
from server.domain.schemas import (
|
|
12
|
+
TopicAdvanceRound,
|
|
13
|
+
TopicCommentCreate,
|
|
14
|
+
TopicCommentRead,
|
|
15
|
+
TopicCommentTreeNode,
|
|
16
|
+
TopicCreate,
|
|
17
|
+
TopicDecisionRead,
|
|
18
|
+
TopicRead,
|
|
19
|
+
TopicResolve,
|
|
20
|
+
TopicSummaryRead,
|
|
21
|
+
TopicUpdate,
|
|
22
|
+
)
|
|
23
|
+
from server.services import notification_service, topic_service
|
|
24
|
+
from server.services import permissions as perm
|
|
25
|
+
from server.services.errors import ForbiddenError
|
|
26
|
+
|
|
27
|
+
topics_router = APIRouter(tags=["topics"], dependencies=[Depends(bind_background_tasks)])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@topics_router.post(
|
|
31
|
+
"/projects/{project_id}/topics",
|
|
32
|
+
response_model=TopicSummaryRead,
|
|
33
|
+
status_code=status.HTTP_201_CREATED,
|
|
34
|
+
)
|
|
35
|
+
def create_topic(
|
|
36
|
+
project_id: uuid.UUID,
|
|
37
|
+
payload: TopicCreate,
|
|
38
|
+
db: Session = Depends(get_db),
|
|
39
|
+
agent: Agent = Depends(get_current_agent),
|
|
40
|
+
) -> TopicSummaryRead:
|
|
41
|
+
resolved_project_id = perm.resolve_project_id_for_agent(agent, project_id)
|
|
42
|
+
perm.ensure_project_access(agent, resolved_project_id)
|
|
43
|
+
topic = topic_service.create_topic(db, resolved_project_id, agent.id, payload)
|
|
44
|
+
emit(
|
|
45
|
+
db,
|
|
46
|
+
agent,
|
|
47
|
+
action="topic.created",
|
|
48
|
+
target_type="topic",
|
|
49
|
+
target_id=topic.id,
|
|
50
|
+
project_id=resolved_project_id,
|
|
51
|
+
summary=f"创建话题「{topic.title}」",
|
|
52
|
+
event="topic.created",
|
|
53
|
+
event_payload={"id": str(topic.id), "title": topic.title},
|
|
54
|
+
)
|
|
55
|
+
return topic_service.topic_summary(db, topic)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@topics_router.get("/projects/{project_id}/topics", response_model=list[TopicSummaryRead])
|
|
59
|
+
def list_topics(
|
|
60
|
+
project_id: uuid.UUID,
|
|
61
|
+
response: Response,
|
|
62
|
+
topic_status: TopicStatus | None = Query(default=None, alias="status"),
|
|
63
|
+
creator_agent_id: uuid.UUID | None = Query(default=None),
|
|
64
|
+
q: str | None = Query(default=None),
|
|
65
|
+
page: int = Query(default=1, ge=1),
|
|
66
|
+
page_size: int = Query(default=100, ge=1, le=100),
|
|
67
|
+
include_archived: bool = Query(default=False),
|
|
68
|
+
db: Session = Depends(get_db),
|
|
69
|
+
agent: Agent = Depends(get_current_agent),
|
|
70
|
+
) -> list[TopicSummaryRead]:
|
|
71
|
+
resolved_project_id = perm.resolve_project_id_for_agent(agent, project_id)
|
|
72
|
+
perm.ensure_project_access(agent, resolved_project_id)
|
|
73
|
+
topics, total = topic_service.list_topics(
|
|
74
|
+
db,
|
|
75
|
+
resolved_project_id,
|
|
76
|
+
status=topic_status,
|
|
77
|
+
creator_agent_id=creator_agent_id,
|
|
78
|
+
q=q,
|
|
79
|
+
page=page,
|
|
80
|
+
page_size=page_size,
|
|
81
|
+
include_archived=include_archived,
|
|
82
|
+
viewer_agent_id=agent.id,
|
|
83
|
+
)
|
|
84
|
+
response.headers["X-Total-Count"] = str(total)
|
|
85
|
+
return topics
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@topics_router.get("/topics/{topic_id}", response_model=TopicRead)
|
|
89
|
+
def get_topic(
|
|
90
|
+
topic_id: uuid.UUID,
|
|
91
|
+
db: Session = Depends(get_db),
|
|
92
|
+
agent: Agent = Depends(get_current_agent),
|
|
93
|
+
) -> TopicRead:
|
|
94
|
+
perm.ensure_topic_access(db, agent, topic_id)
|
|
95
|
+
return topic_service.get_topic_detail(db, topic_id)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@topics_router.patch("/topics/{topic_id}", response_model=TopicSummaryRead)
|
|
99
|
+
def update_topic(
|
|
100
|
+
topic_id: uuid.UUID,
|
|
101
|
+
payload: TopicUpdate,
|
|
102
|
+
db: Session = Depends(get_db),
|
|
103
|
+
agent: Agent = Depends(get_current_agent),
|
|
104
|
+
) -> TopicSummaryRead:
|
|
105
|
+
# Archive/undo is a project-level operation: any project member may archive
|
|
106
|
+
# or restore a topic (docs/CLI.md archive spec — mirrors the experiment side
|
|
107
|
+
# which uses ``ensure_experiment_access``). Other field edits (title /
|
|
108
|
+
# description / pinned) remain host/admin-only.
|
|
109
|
+
changed_fields = payload.model_dump(exclude_unset=True)
|
|
110
|
+
if set(changed_fields) <= {"archived"}:
|
|
111
|
+
perm.ensure_topic_access(db, agent, topic_id)
|
|
112
|
+
else:
|
|
113
|
+
perm.ensure_topic_host_or_admin(db, agent, topic_id)
|
|
114
|
+
topic = topic_service.update_topic(db, topic_id, payload)
|
|
115
|
+
return topic_service.topic_summary(db, topic)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@topics_router.delete("/topics/{topic_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
119
|
+
def delete_topic(
|
|
120
|
+
topic_id: uuid.UUID,
|
|
121
|
+
db: Session = Depends(get_db),
|
|
122
|
+
agent: Agent = Depends(get_current_agent),
|
|
123
|
+
) -> None:
|
|
124
|
+
perm.ensure_topic_host_or_admin(db, agent, topic_id)
|
|
125
|
+
topic_service.soft_delete_topic(db, topic_id)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@topics_router.post("/topics/{topic_id}/close", response_model=TopicSummaryRead)
|
|
129
|
+
def close_topic(
|
|
130
|
+
topic_id: uuid.UUID,
|
|
131
|
+
db: Session = Depends(get_db),
|
|
132
|
+
agent: Agent = Depends(get_current_agent),
|
|
133
|
+
) -> TopicSummaryRead:
|
|
134
|
+
perm.ensure_topic_host_or_admin(db, agent, topic_id)
|
|
135
|
+
topic = topic_service.set_topic_status(db, topic_id, TopicStatus.closed)
|
|
136
|
+
# Phase 2 D2: kind-directed SSE so the waker can map to ``topic_lifecycle``.
|
|
137
|
+
notification_service.emit_kind(
|
|
138
|
+
db,
|
|
139
|
+
project_id=topic.project_id,
|
|
140
|
+
actor_id=agent.id,
|
|
141
|
+
personas=["host", "participant"],
|
|
142
|
+
event="topic.lifecycle.closed",
|
|
143
|
+
summary=f"话题已关闭「{topic.title}」",
|
|
144
|
+
target_type="topic",
|
|
145
|
+
target_id=topic.id,
|
|
146
|
+
payload={"topic_id": str(topic.id), "title": topic.title, "status": "closed"},
|
|
147
|
+
)
|
|
148
|
+
return topic_service.topic_summary(db, topic)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@topics_router.post("/topics/{topic_id}/dismiss", response_model=TopicSummaryRead)
|
|
152
|
+
def dismiss_my_topic(
|
|
153
|
+
topic_id: uuid.UUID,
|
|
154
|
+
db: Session = Depends(get_db),
|
|
155
|
+
agent: Agent = Depends(get_current_agent),
|
|
156
|
+
) -> TopicSummaryRead:
|
|
157
|
+
"""Host-only: hide an open topic from the creator's /todos.
|
|
158
|
+
|
|
159
|
+
Auto re-surfaces when the topic gets new activity (e.g. new comments).
|
|
160
|
+
"""
|
|
161
|
+
topic = topic_service.dismiss_topic(db, agent=agent, topic_id=topic_id)
|
|
162
|
+
if topic is None:
|
|
163
|
+
raise HTTPException(status_code=404, detail="Topic not found")
|
|
164
|
+
return topic_service.topic_summary(db, topic)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@topics_router.post("/topics/{topic_id}/reopen", response_model=TopicSummaryRead)
|
|
168
|
+
def reopen_topic(
|
|
169
|
+
topic_id: uuid.UUID,
|
|
170
|
+
db: Session = Depends(get_db),
|
|
171
|
+
agent: Agent = Depends(get_current_agent),
|
|
172
|
+
) -> TopicSummaryRead:
|
|
173
|
+
perm.ensure_topic_host_or_admin(db, agent, topic_id)
|
|
174
|
+
topic = topic_service.set_topic_status(db, topic_id, TopicStatus.open)
|
|
175
|
+
# Phase 2 D2: kind-directed SSE for reopen so the waker can map to
|
|
176
|
+
# ``topic_lifecycle`` and resume the participant wake loop.
|
|
177
|
+
notification_service.emit_kind(
|
|
178
|
+
db,
|
|
179
|
+
project_id=topic.project_id,
|
|
180
|
+
actor_id=agent.id,
|
|
181
|
+
personas=["host", "participant"],
|
|
182
|
+
event="topic.lifecycle.reopened",
|
|
183
|
+
summary=f"话题已重开「{topic.title}」",
|
|
184
|
+
target_type="topic",
|
|
185
|
+
target_id=topic.id,
|
|
186
|
+
payload={"topic_id": str(topic.id), "title": topic.title, "status": "open"},
|
|
187
|
+
)
|
|
188
|
+
return topic_service.topic_summary(db, topic)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@topics_router.post("/topics/{topic_id}/advance-round", response_model=TopicSummaryRead)
|
|
192
|
+
def advance_topic_round(
|
|
193
|
+
topic_id: uuid.UUID,
|
|
194
|
+
payload: TopicAdvanceRound | None = None,
|
|
195
|
+
db: Session = Depends(get_db),
|
|
196
|
+
agent: Agent = Depends(get_current_agent),
|
|
197
|
+
) -> TopicSummaryRead:
|
|
198
|
+
topic = perm.ensure_topic_access(db, agent, topic_id)
|
|
199
|
+
body = payload or TopicAdvanceRound()
|
|
200
|
+
|
|
201
|
+
if body.ack is not None:
|
|
202
|
+
topic = topic_service.record_participant_round_ack(db, topic_id, agent, body.ack)
|
|
203
|
+
return topic_service.topic_summary(db, topic)
|
|
204
|
+
|
|
205
|
+
if topic.creator_agent_id != agent.id and not perm.is_admin(agent):
|
|
206
|
+
raise ForbiddenError("Only the topic host or admin can advance the discussion round")
|
|
207
|
+
topic = topic_service.advance_topic_round(
|
|
208
|
+
db,
|
|
209
|
+
topic_id,
|
|
210
|
+
increment_summary=body.increment_summary,
|
|
211
|
+
acknowledged_by=body.acknowledged_by,
|
|
212
|
+
)
|
|
213
|
+
emit(
|
|
214
|
+
db,
|
|
215
|
+
agent,
|
|
216
|
+
action="topic.advance_round",
|
|
217
|
+
target_type="topic",
|
|
218
|
+
target_id=topic.id,
|
|
219
|
+
project_id=topic.project_id,
|
|
220
|
+
summary=f"推进话题轮次至 {topic.discussion_round.value}",
|
|
221
|
+
event="topic.advance_round",
|
|
222
|
+
event_payload={
|
|
223
|
+
"topic_id": str(topic.id),
|
|
224
|
+
"discussion_round": topic.discussion_round.value,
|
|
225
|
+
"round_summary_count": topic.round_summary_count,
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
return topic_service.topic_summary(db, topic)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@topics_router.post("/topics/{topic_id}/resolve", response_model=TopicDecisionRead)
|
|
232
|
+
def resolve_topic(
|
|
233
|
+
topic_id: uuid.UUID,
|
|
234
|
+
payload: TopicResolve,
|
|
235
|
+
db: Session = Depends(get_db),
|
|
236
|
+
agent: Agent = Depends(get_current_agent),
|
|
237
|
+
) -> TopicDecisionRead:
|
|
238
|
+
topic = perm.ensure_topic_access(db, agent, topic_id)
|
|
239
|
+
if topic.creator_agent_id != agent.id and not perm.is_admin(agent):
|
|
240
|
+
raise ForbiddenError("Only the topic host or admin can resolve the topic")
|
|
241
|
+
decision = topic_service.resolve_topic(db, topic_id, agent, payload)
|
|
242
|
+
emit(
|
|
243
|
+
db,
|
|
244
|
+
agent,
|
|
245
|
+
action="topic.resolved",
|
|
246
|
+
target_type="topic",
|
|
247
|
+
target_id=topic.id,
|
|
248
|
+
project_id=topic.project_id,
|
|
249
|
+
summary=f"沉淀话题结论「{topic.title}」",
|
|
250
|
+
event="topic.resolved",
|
|
251
|
+
event_payload={"topic_id": str(topic.id), "decision_id": str(decision.id)},
|
|
252
|
+
)
|
|
253
|
+
return topic_service.topic_decision_read(db, decision)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@topics_router.post(
|
|
257
|
+
"/topics/{topic_id}/comments",
|
|
258
|
+
response_model=TopicCommentRead,
|
|
259
|
+
status_code=status.HTTP_201_CREATED,
|
|
260
|
+
)
|
|
261
|
+
def create_topic_comment(
|
|
262
|
+
topic_id: uuid.UUID,
|
|
263
|
+
payload: TopicCommentCreate,
|
|
264
|
+
db: Session = Depends(get_db),
|
|
265
|
+
agent: Agent = Depends(get_current_agent),
|
|
266
|
+
) -> TopicCommentRead:
|
|
267
|
+
topic = perm.ensure_topic_access(db, agent, topic_id)
|
|
268
|
+
comment, unresolved = topic_service.create_topic_comment(db, topic_id, agent, payload)
|
|
269
|
+
event_payload = {"topic_id": str(topic_id), "comment_id": str(comment.id)}
|
|
270
|
+
emit(
|
|
271
|
+
db,
|
|
272
|
+
agent,
|
|
273
|
+
action="topic.comment.created",
|
|
274
|
+
target_type="topic_comment",
|
|
275
|
+
target_id=comment.id,
|
|
276
|
+
project_id=topic.project_id,
|
|
277
|
+
summary="话题新评论",
|
|
278
|
+
event="topic.comment.created",
|
|
279
|
+
event_payload=event_payload,
|
|
280
|
+
notify=False,
|
|
281
|
+
)
|
|
282
|
+
notification_service.notify_topic_comment_created(
|
|
283
|
+
db,
|
|
284
|
+
project_id=topic.project_id,
|
|
285
|
+
actor_id=agent.id,
|
|
286
|
+
creator_agent_id=topic.creator_agent_id,
|
|
287
|
+
target_id=comment.id,
|
|
288
|
+
payload=event_payload,
|
|
289
|
+
)
|
|
290
|
+
return topic_service.topic_comment_read(db, comment, unresolved_mentions=unresolved)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@topics_router.get("/topics/{topic_id}/comments")
|
|
294
|
+
def list_topic_comments(
|
|
295
|
+
topic_id: uuid.UUID,
|
|
296
|
+
tree: bool = Query(default=False),
|
|
297
|
+
limit: int = Query(default=100, ge=1, le=500),
|
|
298
|
+
db: Session = Depends(get_db),
|
|
299
|
+
agent: Agent = Depends(get_current_agent),
|
|
300
|
+
) -> list[TopicCommentRead] | list[TopicCommentTreeNode]:
|
|
301
|
+
perm.ensure_topic_access(db, agent, topic_id)
|
|
302
|
+
return topic_service.list_topic_comments(db, topic_id, tree=tree, limit=limit)
|