205 lines
6.6 KiB
TypeScript
205 lines
6.6 KiB
TypeScript
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";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DB helpers — atlas_conversations table
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let tableReady = false;
|
|
|
|
async function ensureTable() {
|
|
if (tableReady) return;
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS atlas_conversations (
|
|
project_id TEXT PRIMARY KEY,
|
|
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
tableReady = true;
|
|
}
|
|
|
|
async function loadAtlasHistory(projectId: string): Promise<any[]> {
|
|
try {
|
|
await ensureTable();
|
|
const rows = await query<{ messages: any[] }>(
|
|
`SELECT messages FROM atlas_conversations WHERE project_id = $1`,
|
|
[projectId]
|
|
);
|
|
return rows[0]?.messages ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function saveAtlasHistory(projectId: string, messages: any[]): Promise<void> {
|
|
try {
|
|
await ensureTable();
|
|
await query(
|
|
`INSERT INTO atlas_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("[atlas-chat] Failed to save history:", e);
|
|
}
|
|
}
|
|
|
|
async function savePrd(projectId: string, prdContent: string): Promise<void> {
|
|
try {
|
|
await query(
|
|
`UPDATE fs_projects
|
|
SET data = data || jsonb_build_object('prd', $2::text, 'stage', 'architecture'),
|
|
updated_at = NOW()
|
|
WHERE id = $1`,
|
|
[projectId, prdContent]
|
|
);
|
|
console.log(`[atlas-chat] PRD saved for project ${projectId}`);
|
|
} catch (e) {
|
|
console.error("[atlas-chat] Failed to save PRD:", e);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET — load stored conversation messages for display
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function GET(
|
|
_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 history = await loadAtlasHistory(projectId);
|
|
|
|
// Filter to only user/assistant messages (no system prompts) for display
|
|
const messages = history
|
|
.filter((m: any) => m.role === "user" || m.role === "assistant")
|
|
.map((m: any) => ({ role: m.role as "user" | "assistant", content: m.content as string }));
|
|
|
|
return NextResponse.json({ messages });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST — send message to Atlas
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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 });
|
|
}
|
|
|
|
const sessionId = `atlas_${projectId}`;
|
|
|
|
// Load conversation history from DB to persist across agent runner restarts.
|
|
// Strip tool_call / tool_response messages — replaying them across sessions
|
|
// causes Gemini to reject the request with a turn-ordering error.
|
|
const rawHistory = await loadAtlasHistory(projectId);
|
|
const history = rawHistory.filter((m: any) =>
|
|
(m.role === "user" || m.role === "assistant") && m.content
|
|
);
|
|
|
|
// __init__ is a special internal trigger used only when there is no existing history.
|
|
// If history already exists, ignore the init request (conversation already started).
|
|
const isInit = message.trim() === "__atlas_init__";
|
|
if (isInit && history.length > 0) {
|
|
return NextResponse.json({ reply: null, alreadyStarted: true });
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
// For init, send the greeting prompt but don't store it as a user message
|
|
message: isInit
|
|
? "Begin the conversation. Introduce yourself as Atlas and ask what the user is building. Do not acknowledge this as an internal trigger."
|
|
: message,
|
|
session_id: sessionId,
|
|
history,
|
|
is_init: isInit,
|
|
}),
|
|
signal: AbortSignal.timeout(120_000),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
console.error("[atlas-chat] Agent runner error:", text);
|
|
return NextResponse.json(
|
|
{ error: "Atlas is unavailable. Please try again." },
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
// Persist updated history
|
|
await saveAtlasHistory(projectId, data.history ?? []);
|
|
|
|
// If Atlas finalized the PRD, save it to the project
|
|
if (data.prdContent) {
|
|
await savePrd(projectId, data.prdContent);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
reply: data.reply,
|
|
sessionId,
|
|
prdContent: data.prdContent ?? null,
|
|
model: data.model ?? null,
|
|
});
|
|
} catch (err) {
|
|
console.error("[atlas-chat] Error:", err);
|
|
return NextResponse.json(
|
|
{ error: "Request timed out or failed. Please try again." },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DELETE — clear Atlas conversation for this project
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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 = `atlas_${projectId}`;
|
|
|
|
try {
|
|
await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${sessionId}`, { method: "DELETE" });
|
|
} catch { /* runner may be down */ }
|
|
|
|
try {
|
|
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]);
|
|
} catch { /* table may not exist yet */ }
|
|
|
|
return NextResponse.json({ cleared: true });
|
|
}
|