import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; // --------------------------------------------------------------------------- // Helpers — chat_conversations + fs_knowledge_items // --------------------------------------------------------------------------- async function loadConversation(projectId: string): Promise { try { const rows = await query<{ messages: any[] }>( `SELECT messages FROM chat_conversations WHERE project_id = $1`, [projectId] ); return rows[0]?.messages ?? []; } catch { return []; } } async function saveConversation(projectId: string, messages: any[]): Promise { try { await query( `INSERT INTO chat_conversations (project_id, messages, updated_at) VALUES ($1, $2::jsonb, NOW()) ON CONFLICT (project_id) DO UPDATE SET messages = $2::jsonb, updated_at = NOW()`, [projectId, JSON.stringify(messages)] ); } catch (e) { console.error("[agent-chat] Failed to save conversation:", e); } } async function loadKnowledge(projectId: string): Promise { try { const rows = await query<{ data: any }>( `SELECT data FROM fs_knowledge_items WHERE project_id = $1 ORDER BY updated_at DESC LIMIT 50`, [projectId] ); if (rows.length === 0) return ""; return rows .map((r) => `[${r.data.type ?? "note"}] ${r.data.key}: ${r.data.value}`) .join("\n"); } catch { return ""; } } async function saveMemoryUpdates( projectId: string, updates: Array<{ key: string; type: string; value: string }> ): Promise { if (!updates?.length) return; try { for (const u of updates) { // Upsert by project_id + key const existing = await query<{ id: string }>( `SELECT id FROM fs_knowledge_items WHERE project_id = $1 AND data->>'key' = $2 LIMIT 1`, [projectId, u.key] ); if (existing.length > 0) { await query( `UPDATE fs_knowledge_items SET data = $1::jsonb, updated_at = NOW() WHERE id = $2`, [JSON.stringify({ key: u.key, type: u.type, value: u.value, source: "ai" }), existing[0].id] ); } else { await query( `INSERT INTO fs_knowledge_items (project_id, data) VALUES ($1, $2::jsonb)`, [projectId, JSON.stringify({ key: u.key, type: u.type, value: u.value, source: "ai" })] ); } } } catch (e) { console.error("[agent-chat] Failed to save memory updates:", e); } } // --------------------------------------------------------------------------- // POST — send a message // --------------------------------------------------------------------------- export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; const { message } = await req.json(); if (!message?.trim()) { return NextResponse.json({ error: '"message" is required' }, { status: 400 }); } // Load project context let projectContext = ""; try { const rows = await query<{ data: any }>( `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, [projectId] ); if (rows.length > 0) { const p = rows[0].data; const lines = [ `Project: ${p.productName ?? p.name ?? "Unnamed"}`, p.productVision ? `Vision: ${p.productVision}` : null, p.giteaRepo ? `Gitea repo: ${p.giteaRepo}` : null, p.coolifyAppUuid ? `Coolify app UUID: ${p.coolifyAppUuid}` : null, p.deploymentUrl ? `Live URL: ${p.deploymentUrl}` : null, p.theiaWorkspaceUrl ? `IDE: ${p.theiaWorkspaceUrl}` : null, ].filter(Boolean); projectContext = lines.join("\n"); } } catch { /* non-fatal */ } const sessionId = `project_${projectId}`; // Load persistent conversation history and knowledge from DB const [history, knowledgeContext] = await Promise.all([ loadConversation(projectId), loadKnowledge(projectId), ]); // Enrich user message with project context on the very first message const isFirstMessage = history.length === 0; const enrichedMessage = isFirstMessage && projectContext ? `[Project context]\n${projectContext}\n\n[User message]\n${message}` : message; try { const res = await fetch(`${AGENT_RUNNER_URL}/orchestrator/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: enrichedMessage, session_id: sessionId, history, knowledge_context: knowledgeContext || undefined, }), signal: AbortSignal.timeout(120_000), }); if (!res.ok) { const errText = await res.text(); return NextResponse.json( { error: `Agent runner error: ${res.status} — ${errText.slice(0, 200)}` }, { status: 502 } ); } const data = await res.json(); // Persist conversation and any memory updates the AI generated await Promise.all([ saveConversation(projectId, data.history ?? []), saveMemoryUpdates(projectId, data.memoryUpdates ?? []), ]); return NextResponse.json({ reply: data.reply, reasoning: data.reasoning ?? null, toolCalls: data.toolCalls ?? [], turns: data.turns ?? 0, model: data.model || null, sessionId, memoryUpdates: data.memoryUpdates ?? [], }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return NextResponse.json( { error: msg.includes("fetch") ? "Agent runner is offline" : msg }, { status: 503 } ); } } // --------------------------------------------------------------------------- // DELETE — clear session + conversation history // --------------------------------------------------------------------------- export async function DELETE( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; const sessionId = `project_${projectId}`; await Promise.all([ // Clear in-memory session from agent runner fetch(`${AGENT_RUNNER_URL}/orchestrator/sessions/${sessionId}`, { method: "DELETE" }).catch(() => {}), // Clear persisted conversation from DB query(`DELETE FROM chat_conversations WHERE project_id = $1`, [projectId]).catch(() => {}), ]); return NextResponse.json({ cleared: true }); }