File size: 12,204 Bytes
bd161ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
"""
Phase management router for the 14-phase workflow system.
"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, update
import logging
from sqlalchemy.ext.asyncio import AsyncSession

from database import get_async_db
from models import User, Project, Phase, PhaseDraft, PhaseStatus
from schemas import (
    PhaseResponse, PhaseUpdate, PhaseGenerateRequest, PhaseGenerateResponse,
    PhaseDraftResponse, APIResponse
)
from dependencies import get_current_active_user, check_project_access
from services.phase_service import PhaseService
from services.rag_service import RAGService

logger = logging.getLogger(__name__)

router = APIRouter()


@router.get("/projects/{project_id}/phases", response_model=List[PhaseResponse])
async def get_project_phases(
    project_id: str,
    project: Project = Depends(check_project_access),
    db: AsyncSession = Depends(get_async_db)
):
    """Get all phases for a project (async)."""
    result = await db.execute(select(Phase).where(Phase.project_id == project_id).order_by(Phase.phase_number))
    phases = result.scalars().all()
    return phases


@router.get("/projects/{project_id}/phases/{phase_number}", response_model=PhaseResponse)
async def get_phase(
    project_id: str,
    phase_number: int,
    project: Project = Depends(check_project_access),
    db: AsyncSession = Depends(get_async_db)
):
    """Get a specific phase."""
    if not (1 <= phase_number <= 14):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Phase number must be between 1 and 14"
        )
    
    result = await db.execute(select(Phase).where(Phase.project_id == project_id, Phase.phase_number == phase_number))
    phase = result.scalar_one_or_none()
    
    if not phase:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Phase not found"
        )
    
    return phase


@router.put("/projects/{project_id}/phases/{phase_number}", response_model=PhaseResponse)
async def update_phase(
    project_id: str,
    phase_number: int,
    phase_data: PhaseUpdate,
    project: Project = Depends(check_project_access),
    current_user: User = Depends(get_current_active_user),
    db: AsyncSession = Depends(get_async_db)
):
    """Update a phase."""
    if not (1 <= phase_number <= 14):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Phase number must be between 1 and 14"
        )
    
    # Get phase
    result = await db.execute(select(Phase).where(Phase.project_id == project_id, Phase.phase_number == phase_number))
    phase = result.scalar_one_or_none()
    
    if not phase:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Phase not found"
        )
    
    # Save current state as draft if there's content
    if phase.ai_response and phase.user_input:
        await PhaseService.save_draft(db, phase.id, phase.user_input, phase.ai_response)
    
    # Update phase
    update_data = {}
    if phase_data.title is not None:
        update_data["title"] = phase_data.title
    if phase_data.description is not None:
        update_data["description"] = phase_data.description
    if phase_data.user_input is not None:
        update_data["user_input"] = phase_data.user_input
    if phase_data.prompt_template is not None:
        update_data["prompt_template"] = phase_data.prompt_template
    
    if update_data:
        await db.execute(
            update(Phase)
            .where(Phase.id == phase.id)
            .values(**update_data)
        )
        
        # Mark subsequent phases as stale if this phase was completed
        if phase.status == PhaseStatus.COMPLETED:
            await PhaseService.mark_subsequent_phases_stale(db, project_id, phase_number)
    
    await db.commit()
    
    # Return updated phase
    result = await db.execute(
        select(Phase)
        .where(Phase.id == phase.id)
    )
    updated_phase = result.scalar_one()
    
    logger.info(f"Phase {phase_number} updated in project {project_id} by {current_user.email}")
    
    return updated_phase


@router.post("/projects/{project_id}/phases/{phase_number}/generate", response_model=PhaseGenerateResponse)
async def generate_phase_content(
    project_id: str,
    phase_number: int,
    request: PhaseGenerateRequest,
    project: Project = Depends(check_project_access),
    current_user: User = Depends(get_current_active_user),
    db: AsyncSession = Depends(get_async_db)
):
    """Generate AI content for a phase."""
    if not (1 <= phase_number <= 14):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Phase number must be between 1 and 14"
        )
    
    # Get phase
    result = await db.execute(select(Phase).where(Phase.project_id == project_id, Phase.phase_number == phase_number))
    phase = result.scalar_one_or_none()
    
    if not phase:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Phase not found"
        )
    
    try:
        # Save current state as draft if there's content
        if phase.ai_response and phase.user_input:
            # TODO: Implement sync version of save_draft
            pass
        
        # Generate content using PhaseService
        ai_response, context_used = await PhaseService.generate_content(
            db, phase, request.user_input, request.use_rag, request.temperature
        )
        
        # Update phase
        await db.execute(
            update(Phase)
            .where(Phase.id == phase.id)
            .values(
                user_input=request.user_input,
                ai_response=ai_response,
                status=PhaseStatus.COMPLETED
            )
        )
        
        # Create embedding for RAG
        if request.use_rag:
            await RAGService.create_embedding(db, phase.id, ai_response)
        
        # Mark subsequent phases as stale
        await PhaseService.mark_subsequent_phases_stale(db, project_id, phase_number)
        
        await db.commit()
        
        logger.info(f"Content generated for phase {phase_number} in project {project_id}")
        
        return PhaseGenerateResponse(
            phase_id=phase.id,
            ai_response=ai_response,
            status=PhaseStatus.COMPLETED,
            context_used=context_used
        )
        
    except Exception as e:
        logger.error(f"Error generating content: {e}")
        await db.rollback()
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to generate content: {str(e)}"
        )


@router.post("/projects/{project_id}/phases/{phase_number}/reconstruct-context", response_model=PhaseGenerateResponse)
async def reconstruct_phase_context(
    project_id: str,
    phase_number: int,
    project: Project = Depends(check_project_access),
    current_user: User = Depends(get_current_active_user),
    db: AsyncSession = Depends(get_async_db)
):
    """Reconstruct context for a phase and regenerate content."""
    if not (1 <= phase_number <= 14):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Phase number must be between 1 and 14"
        )
    
    # Get phase
    result = await db.execute(select(Phase).where(Phase.project_id == project_id, Phase.phase_number == phase_number))
    phase = result.scalar_one_or_none()
    
    if not phase:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Phase not found"
        )
    
    if not phase.user_input:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Phase has no user input to reconstruct from"
        )
    
    try:
        # Save current state as draft
        if phase.ai_response:
            await PhaseService.save_draft(db, phase.id, phase.user_input, phase.ai_response)
        
        # Reconstruct context and regenerate
        ai_response, context_used = await PhaseService.generate_content(
            db, phase, phase.user_input, use_rag=True, temperature=0.7
        )
        
        # Update phase
        await db.execute(
            update(Phase)
            .where(Phase.id == phase.id)
            .values(
                ai_response=ai_response,
                status=PhaseStatus.COMPLETED
            )
        )
        
        # Update embedding
        await RAGService.create_embedding(db, phase.id, ai_response)
        
        await db.commit()
        
        logger.info(f"Context reconstructed for phase {phase_number} in project {project_id}")
        
        return PhaseGenerateResponse(
            phase_id=phase.id,
            ai_response=ai_response,
            status=PhaseStatus.COMPLETED,
            context_used=context_used
        )
        
    except Exception as e:
        logger.error(f"Error reconstructing context: {e}")
        await db.rollback()
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to reconstruct context: {str(e)}"
        )


@router.get("/projects/{project_id}/phases/{phase_number}/drafts", response_model=List[PhaseDraftResponse])
async def get_phase_drafts(
    project_id: str,
    phase_number: int,
    project: Project = Depends(check_project_access),
    db: AsyncSession = Depends(get_async_db)
):
    """Get all drafts for a phase."""
    # Get phase
    result = await db.execute(select(Phase).where(Phase.project_id == project_id, Phase.phase_number == phase_number))
    phase = result.scalar_one_or_none()
    
    if not phase:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Phase not found"
        )
    
    # Get drafts
    drafts_result = await db.execute(
        select(PhaseDraft)
        .where(PhaseDraft.phase_id == phase.id)
        .order_by(PhaseDraft.version.desc())
    )
    drafts = drafts_result.scalars().all()
    
    return drafts


@router.post("/projects/{project_id}/phases/{phase_number}/drafts/{version}/restore", response_model=PhaseResponse)
async def restore_phase_draft(
    project_id: str,
    phase_number: int,
    version: int,
    project: Project = Depends(check_project_access),
    current_user: User = Depends(get_current_active_user),
    db: AsyncSession = Depends(get_async_db)
):
    """Restore a phase from a specific draft version."""
    # Get phase
    result = await db.execute(select(Phase).where(Phase.project_id == project_id, Phase.phase_number == phase_number))
    phase = result.scalar_one_or_none()
    
    if not phase:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Phase not found"
        )
    
    # Get draft
    draft_result = await db.execute(
        select(PhaseDraft).where(
            PhaseDraft.phase_id == phase.id,
            PhaseDraft.version == version
        )
    )
    draft = draft_result.scalar_one_or_none()
    
    if not draft:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Draft not found"
        )
    
    # Save current state as new draft
    if phase.ai_response and phase.user_input:
        await PhaseService.save_draft(db, phase.id, phase.user_input, phase.ai_response)
    
    # Restore from draft
    await db.execute(
        update(Phase)
        .where(Phase.id == phase.id)
        .values(
            user_input=draft.user_input,
            ai_response=draft.ai_response
        )
    )
    
    # Mark subsequent phases as stale
    await PhaseService.mark_subsequent_phases_stale(db, project_id, phase_number)
    
    await db.commit()
    
    # Return updated phase
    result = await db.execute(
        select(Phase)
        .where(Phase.id == phase.id)
    )
    updated_phase = result.scalar_one()
    
    logger.info(f"Phase {phase_number} restored to version {version} in project {project_id}")
    
    return updated_phase