cc-discussion 1.0.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.
- backend/__init__.py +1 -0
- backend/__main__.py +7 -0
- backend/cli.py +44 -0
- backend/main.py +167 -0
- backend/models/__init__.py +2 -0
- backend/models/database.py +244 -0
- backend/routers/__init__.py +3 -0
- backend/routers/history.py +202 -0
- backend/routers/rooms.py +377 -0
- backend/services/__init__.py +1 -0
- backend/services/codex_agent.py +357 -0
- backend/services/codex_history_reader.py +287 -0
- backend/services/discussion_orchestrator.py +461 -0
- backend/services/history_reader.py +455 -0
- backend/services/meeting_prompts.py +291 -0
- backend/services/parallel_orchestrator.py +908 -0
- backend/services/participant_agent.py +394 -0
- backend/websocket.py +292 -0
- cc_discussion-1.0.0.dist-info/METADATA +527 -0
- cc_discussion-1.0.0.dist-info/RECORD +23 -0
- cc_discussion-1.0.0.dist-info/WHEEL +5 -0
- cc_discussion-1.0.0.dist-info/entry_points.txt +2 -0
- cc_discussion-1.0.0.dist-info/top_level.txt +1 -0
backend/routers/rooms.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rooms Router
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
API endpoints for managing discussion rooms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
from sqlalchemy.orm import Session, joinedload
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..models.database import (
|
|
17
|
+
DiscussionRoom,
|
|
18
|
+
RoomParticipant,
|
|
19
|
+
DiscussionMessage,
|
|
20
|
+
RoomStatus,
|
|
21
|
+
MeetingType,
|
|
22
|
+
AgentType,
|
|
23
|
+
get_db,
|
|
24
|
+
)
|
|
25
|
+
from ..services.history_reader import decode_project_id, get_original_path_from_dir
|
|
26
|
+
from ..services.codex_history_reader import _decode_path as decode_codex_path
|
|
27
|
+
|
|
28
|
+
router = APIRouter(prefix="/api/rooms", tags=["rooms"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_project_name(participant: RoomParticipant) -> Optional[str]:
|
|
32
|
+
"""Get project name from participant's context_project_dir."""
|
|
33
|
+
if not participant.context_project_dir:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
agent_type = participant.agent_type.value if participant.agent_type else "claude"
|
|
38
|
+
|
|
39
|
+
if agent_type == "codex":
|
|
40
|
+
# Codex: context_project_dir is base64-encoded path
|
|
41
|
+
actual_path = decode_codex_path(participant.context_project_dir)
|
|
42
|
+
if actual_path:
|
|
43
|
+
return Path(actual_path).name
|
|
44
|
+
else:
|
|
45
|
+
# Claude: context_project_dir is encoded project ID
|
|
46
|
+
internal_dir = Path(decode_project_id(participant.context_project_dir))
|
|
47
|
+
actual_path = get_original_path_from_dir(internal_dir)
|
|
48
|
+
if actual_path:
|
|
49
|
+
return Path(actual_path).name
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Request/Response Models
|
|
57
|
+
|
|
58
|
+
class ParticipantCreate(BaseModel):
|
|
59
|
+
"""Request model for creating a participant."""
|
|
60
|
+
name: str = Field(..., min_length=1, max_length=50)
|
|
61
|
+
role: Optional[str] = Field(None, max_length=100)
|
|
62
|
+
color: str = Field(default="#6366f1", pattern=r"^#[0-9a-fA-F]{6}$")
|
|
63
|
+
context_project_dir: Optional[str] = None
|
|
64
|
+
context_session_id: Optional[str] = None
|
|
65
|
+
is_facilitator: bool = False
|
|
66
|
+
agent_type: str = Field(default="claude", pattern=r"^(claude|codex)$")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RoomCreate(BaseModel):
|
|
70
|
+
"""Request model for creating a room."""
|
|
71
|
+
name: str = Field(..., min_length=1, max_length=200)
|
|
72
|
+
topic: Optional[str] = None
|
|
73
|
+
max_turns: int = Field(default=20, ge=1, le=100)
|
|
74
|
+
meeting_type: str = Field(default="technical_review")
|
|
75
|
+
custom_meeting_description: Optional[str] = None # "その他"選択時のカスタム説明
|
|
76
|
+
language: str = Field(default="ja", pattern=r"^(ja|en)$")
|
|
77
|
+
participants: List[ParticipantCreate] = Field(..., min_length=2, max_length=3)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ModeratorMessage(BaseModel):
|
|
81
|
+
"""Request model for moderator intervention."""
|
|
82
|
+
content: str = Field(..., min_length=1)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RoomResponse(BaseModel):
|
|
86
|
+
"""Response model for a room."""
|
|
87
|
+
id: int
|
|
88
|
+
name: str
|
|
89
|
+
topic: Optional[str]
|
|
90
|
+
status: str
|
|
91
|
+
current_turn: int
|
|
92
|
+
max_turns: int
|
|
93
|
+
meeting_type: str
|
|
94
|
+
custom_meeting_description: Optional[str]
|
|
95
|
+
language: str
|
|
96
|
+
participant_count: int
|
|
97
|
+
created_at: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ParticipantResponse(BaseModel):
|
|
101
|
+
"""Response model for a participant."""
|
|
102
|
+
id: int
|
|
103
|
+
name: str
|
|
104
|
+
role: Optional[str]
|
|
105
|
+
color: str
|
|
106
|
+
has_context: bool
|
|
107
|
+
is_speaking: bool
|
|
108
|
+
message_count: int
|
|
109
|
+
is_facilitator: bool
|
|
110
|
+
agent_type: str
|
|
111
|
+
project_name: Optional[str] = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MessageResponse(BaseModel):
|
|
115
|
+
"""Response model for a message."""
|
|
116
|
+
id: int
|
|
117
|
+
participant_id: Optional[int]
|
|
118
|
+
role: str
|
|
119
|
+
content: str
|
|
120
|
+
turn_number: int
|
|
121
|
+
created_at: str
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class RoomDetailResponse(BaseModel):
|
|
125
|
+
"""Detailed response model for a room."""
|
|
126
|
+
id: int
|
|
127
|
+
name: str
|
|
128
|
+
topic: Optional[str]
|
|
129
|
+
status: str
|
|
130
|
+
current_turn: int
|
|
131
|
+
max_turns: int
|
|
132
|
+
meeting_type: str
|
|
133
|
+
custom_meeting_description: Optional[str]
|
|
134
|
+
language: str
|
|
135
|
+
created_at: str
|
|
136
|
+
participants: List[ParticipantResponse]
|
|
137
|
+
messages: List[MessageResponse]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Endpoints
|
|
141
|
+
|
|
142
|
+
@router.post("", response_model=RoomResponse)
|
|
143
|
+
async def create_room(room_data: RoomCreate, db: Session = Depends(get_db)):
|
|
144
|
+
"""Create a new discussion room with participants."""
|
|
145
|
+
# Validate participant count
|
|
146
|
+
if len(room_data.participants) < 2 or len(room_data.participants) > 3:
|
|
147
|
+
raise HTTPException(
|
|
148
|
+
status_code=400,
|
|
149
|
+
detail="Rooms must have 2-3 participants"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Parse meeting_type
|
|
153
|
+
try:
|
|
154
|
+
meeting_type_enum = MeetingType(room_data.meeting_type)
|
|
155
|
+
except ValueError:
|
|
156
|
+
meeting_type_enum = MeetingType.TECHNICAL_REVIEW
|
|
157
|
+
|
|
158
|
+
# Create room
|
|
159
|
+
room = DiscussionRoom(
|
|
160
|
+
name=room_data.name,
|
|
161
|
+
topic=room_data.topic,
|
|
162
|
+
max_turns=room_data.max_turns,
|
|
163
|
+
meeting_type=meeting_type_enum,
|
|
164
|
+
custom_meeting_description=room_data.custom_meeting_description,
|
|
165
|
+
language=room_data.language,
|
|
166
|
+
)
|
|
167
|
+
db.add(room)
|
|
168
|
+
db.flush()
|
|
169
|
+
|
|
170
|
+
# Create participants
|
|
171
|
+
for p_data in room_data.participants:
|
|
172
|
+
# Parse agent_type
|
|
173
|
+
try:
|
|
174
|
+
agent_type_enum = AgentType(p_data.agent_type)
|
|
175
|
+
except ValueError:
|
|
176
|
+
agent_type_enum = AgentType.CLAUDE
|
|
177
|
+
|
|
178
|
+
participant = RoomParticipant(
|
|
179
|
+
room_id=room.id,
|
|
180
|
+
name=p_data.name,
|
|
181
|
+
role=p_data.role,
|
|
182
|
+
color=p_data.color,
|
|
183
|
+
context_project_dir=p_data.context_project_dir,
|
|
184
|
+
context_session_id=p_data.context_session_id,
|
|
185
|
+
is_facilitator=p_data.is_facilitator,
|
|
186
|
+
agent_type=agent_type_enum,
|
|
187
|
+
)
|
|
188
|
+
db.add(participant)
|
|
189
|
+
|
|
190
|
+
db.commit()
|
|
191
|
+
db.refresh(room)
|
|
192
|
+
|
|
193
|
+
return RoomResponse(
|
|
194
|
+
id=room.id,
|
|
195
|
+
name=room.name,
|
|
196
|
+
topic=room.topic,
|
|
197
|
+
status=room.status.value,
|
|
198
|
+
current_turn=room.current_turn,
|
|
199
|
+
max_turns=room.max_turns,
|
|
200
|
+
meeting_type=room.meeting_type.value if room.meeting_type else "technical_review",
|
|
201
|
+
custom_meeting_description=room.custom_meeting_description,
|
|
202
|
+
language=room.language or "ja",
|
|
203
|
+
participant_count=len(room.participants),
|
|
204
|
+
created_at=room.created_at.isoformat(),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@router.get("", response_model=List[RoomResponse])
|
|
209
|
+
async def list_rooms(db: Session = Depends(get_db)):
|
|
210
|
+
"""List all discussion rooms."""
|
|
211
|
+
rooms = db.query(DiscussionRoom).order_by(
|
|
212
|
+
DiscussionRoom.created_at.desc()
|
|
213
|
+
).all()
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
RoomResponse(
|
|
217
|
+
id=r.id,
|
|
218
|
+
name=r.name,
|
|
219
|
+
topic=r.topic,
|
|
220
|
+
status=r.status.value,
|
|
221
|
+
current_turn=r.current_turn,
|
|
222
|
+
max_turns=r.max_turns,
|
|
223
|
+
meeting_type=r.meeting_type.value if r.meeting_type else "technical_review",
|
|
224
|
+
custom_meeting_description=r.custom_meeting_description,
|
|
225
|
+
language=r.language or "ja",
|
|
226
|
+
participant_count=len(r.participants),
|
|
227
|
+
created_at=r.created_at.isoformat(),
|
|
228
|
+
)
|
|
229
|
+
for r in rooms
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.get("/{room_id}", response_model=RoomDetailResponse)
|
|
234
|
+
async def get_room(room_id: int, db: Session = Depends(get_db)):
|
|
235
|
+
"""Get room details including participants and messages."""
|
|
236
|
+
room = db.query(DiscussionRoom).options(
|
|
237
|
+
joinedload(DiscussionRoom.participants),
|
|
238
|
+
joinedload(DiscussionRoom.messages)
|
|
239
|
+
).filter(
|
|
240
|
+
DiscussionRoom.id == room_id
|
|
241
|
+
).first()
|
|
242
|
+
|
|
243
|
+
if not room:
|
|
244
|
+
raise HTTPException(status_code=404, detail="Room not found")
|
|
245
|
+
|
|
246
|
+
return RoomDetailResponse(
|
|
247
|
+
id=room.id,
|
|
248
|
+
name=room.name,
|
|
249
|
+
topic=room.topic,
|
|
250
|
+
status=room.status.value,
|
|
251
|
+
current_turn=room.current_turn,
|
|
252
|
+
max_turns=room.max_turns,
|
|
253
|
+
meeting_type=room.meeting_type.value if room.meeting_type else "technical_review",
|
|
254
|
+
custom_meeting_description=room.custom_meeting_description,
|
|
255
|
+
language=room.language or "ja",
|
|
256
|
+
created_at=room.created_at.isoformat(),
|
|
257
|
+
participants=[
|
|
258
|
+
ParticipantResponse(
|
|
259
|
+
id=p.id,
|
|
260
|
+
name=p.name,
|
|
261
|
+
role=p.role,
|
|
262
|
+
color=p.color,
|
|
263
|
+
has_context=bool(p.context_session_id),
|
|
264
|
+
is_speaking=p.is_speaking,
|
|
265
|
+
message_count=p.message_count,
|
|
266
|
+
is_facilitator=p.is_facilitator or False,
|
|
267
|
+
agent_type=p.agent_type.value if p.agent_type else "claude",
|
|
268
|
+
project_name=get_project_name(p),
|
|
269
|
+
)
|
|
270
|
+
for p in room.participants
|
|
271
|
+
],
|
|
272
|
+
messages=[
|
|
273
|
+
MessageResponse(
|
|
274
|
+
id=m.id,
|
|
275
|
+
participant_id=m.participant_id,
|
|
276
|
+
role=m.role,
|
|
277
|
+
content=m.content,
|
|
278
|
+
turn_number=m.turn_number,
|
|
279
|
+
created_at=m.created_at.isoformat(),
|
|
280
|
+
)
|
|
281
|
+
for m in sorted(room.messages, key=lambda x: x.created_at)
|
|
282
|
+
],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@router.delete("/{room_id}")
|
|
287
|
+
async def delete_room(room_id: int, db: Session = Depends(get_db)):
|
|
288
|
+
"""Delete a discussion room."""
|
|
289
|
+
room = db.query(DiscussionRoom).filter(
|
|
290
|
+
DiscussionRoom.id == room_id
|
|
291
|
+
).first()
|
|
292
|
+
|
|
293
|
+
if not room:
|
|
294
|
+
raise HTTPException(status_code=404, detail="Room not found")
|
|
295
|
+
|
|
296
|
+
db.delete(room)
|
|
297
|
+
db.commit()
|
|
298
|
+
|
|
299
|
+
return {"status": "deleted", "room_id": room_id}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@router.post("/{room_id}/start")
|
|
303
|
+
async def start_discussion(room_id: int, db: Session = Depends(get_db)):
|
|
304
|
+
"""Start or resume a discussion."""
|
|
305
|
+
room = db.query(DiscussionRoom).filter(
|
|
306
|
+
DiscussionRoom.id == room_id
|
|
307
|
+
).first()
|
|
308
|
+
|
|
309
|
+
if not room:
|
|
310
|
+
raise HTTPException(status_code=404, detail="Room not found")
|
|
311
|
+
|
|
312
|
+
if room.status == RoomStatus.ACTIVE:
|
|
313
|
+
raise HTTPException(status_code=400, detail="Discussion already active")
|
|
314
|
+
|
|
315
|
+
if room.status == RoomStatus.COMPLETED:
|
|
316
|
+
raise HTTPException(status_code=400, detail="Discussion already completed")
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"status": "ready",
|
|
320
|
+
"room_id": room_id,
|
|
321
|
+
"websocket_url": f"/ws/rooms/{room_id}"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@router.post("/{room_id}/pause")
|
|
326
|
+
async def pause_discussion(room_id: int, db: Session = Depends(get_db)):
|
|
327
|
+
"""Pause an active discussion."""
|
|
328
|
+
room = db.query(DiscussionRoom).filter(
|
|
329
|
+
DiscussionRoom.id == room_id
|
|
330
|
+
).first()
|
|
331
|
+
|
|
332
|
+
if not room:
|
|
333
|
+
raise HTTPException(status_code=404, detail="Room not found")
|
|
334
|
+
|
|
335
|
+
if room.status != RoomStatus.ACTIVE:
|
|
336
|
+
raise HTTPException(status_code=400, detail="Discussion not active")
|
|
337
|
+
|
|
338
|
+
room.status = RoomStatus.PAUSED
|
|
339
|
+
db.commit()
|
|
340
|
+
|
|
341
|
+
return {"status": "paused", "room_id": room_id}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@router.post("/{room_id}/moderate")
|
|
345
|
+
async def add_moderator_message(
|
|
346
|
+
room_id: int,
|
|
347
|
+
message: ModeratorMessage,
|
|
348
|
+
db: Session = Depends(get_db)
|
|
349
|
+
):
|
|
350
|
+
"""Add a moderator message to the discussion."""
|
|
351
|
+
room = db.query(DiscussionRoom).filter(
|
|
352
|
+
DiscussionRoom.id == room_id
|
|
353
|
+
).first()
|
|
354
|
+
|
|
355
|
+
if not room:
|
|
356
|
+
raise HTTPException(status_code=404, detail="Room not found")
|
|
357
|
+
|
|
358
|
+
# Create moderator message
|
|
359
|
+
msg = DiscussionMessage(
|
|
360
|
+
room_id=room_id,
|
|
361
|
+
participant_id=None,
|
|
362
|
+
role="moderator",
|
|
363
|
+
content=message.content,
|
|
364
|
+
turn_number=room.current_turn,
|
|
365
|
+
)
|
|
366
|
+
db.add(msg)
|
|
367
|
+
db.commit()
|
|
368
|
+
db.refresh(msg)
|
|
369
|
+
|
|
370
|
+
return MessageResponse(
|
|
371
|
+
id=msg.id,
|
|
372
|
+
participant_id=msg.participant_id,
|
|
373
|
+
role=msg.role,
|
|
374
|
+
content=msg.content,
|
|
375
|
+
turn_number=msg.turn_number,
|
|
376
|
+
created_at=msg.created_at.isoformat(),
|
|
377
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Services for Claude Discussion Room."""
|