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.
@@ -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."""