|
|
|
|
@@ -1,13 +1,11 @@
|
|
|
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
|
import { authSession } from "@/lib/auth/session-server";
|
|
|
|
|
import { query } from "@/lib/db-postgres";
|
|
|
|
|
import {
|
|
|
|
|
augmentAtlasMessage,
|
|
|
|
|
parseContextRefs,
|
|
|
|
|
} from "@/lib/chat-context-refs";
|
|
|
|
|
import { augmentAtlasMessage, parseContextRefs } from "@/lib/chat-context-refs";
|
|
|
|
|
import { formatCreationKickoffForPrompt } from "@/lib/server/creation-kickoff-prompt";
|
|
|
|
|
|
|
|
|
|
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
|
|
|
|
const AGENT_RUNNER_URL =
|
|
|
|
|
process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
|
|
|
|
|
|
|
|
|
const ALLOWED_SCOPES = new Set(["overview", "build"]);
|
|
|
|
|
|
|
|
|
|
@@ -16,8 +14,13 @@ function normalizeScope(raw: string | null | undefined): "overview" | "build" {
|
|
|
|
|
return ALLOWED_SCOPES.has(s) ? (s as "overview" | "build") : "overview";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runnerSessionId(projectId: string, scope: "overview" | "build"): string {
|
|
|
|
|
return scope === "overview" ? `atlas_${projectId}` : `atlas_${projectId}__build`;
|
|
|
|
|
function runnerSessionId(
|
|
|
|
|
projectId: string,
|
|
|
|
|
scope: "overview" | "build",
|
|
|
|
|
): string {
|
|
|
|
|
return scope === "overview"
|
|
|
|
|
? `atlas_${projectId}`
|
|
|
|
|
: `atlas_${projectId}__build`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
@@ -53,12 +56,15 @@ async function ensureLegacyConversationsTable() {
|
|
|
|
|
legacyTableChecked = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadAtlasHistory(projectId: string, scope: "overview" | "build"): Promise<any[]> {
|
|
|
|
|
async function loadAtlasHistory(
|
|
|
|
|
projectId: string,
|
|
|
|
|
scope: "overview" | "build",
|
|
|
|
|
): Promise<any[]> {
|
|
|
|
|
try {
|
|
|
|
|
await ensureThreadsTable();
|
|
|
|
|
const rows = await query<{ messages: any[] }>(
|
|
|
|
|
`SELECT messages FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`,
|
|
|
|
|
[projectId, scope]
|
|
|
|
|
[projectId, scope],
|
|
|
|
|
);
|
|
|
|
|
if (rows.length > 0) {
|
|
|
|
|
const fromThreads = rows[0]?.messages;
|
|
|
|
|
@@ -68,7 +74,7 @@ async function loadAtlasHistory(projectId: string, scope: "overview" | "build"):
|
|
|
|
|
await ensureLegacyConversationsTable();
|
|
|
|
|
const leg = await query<{ messages: any[] }>(
|
|
|
|
|
`SELECT messages FROM atlas_conversations WHERE project_id = $1`,
|
|
|
|
|
[projectId]
|
|
|
|
|
[projectId],
|
|
|
|
|
);
|
|
|
|
|
const legacyMsgs = leg[0]?.messages ?? [];
|
|
|
|
|
if (Array.isArray(legacyMsgs) && legacyMsgs.length > 0) {
|
|
|
|
|
@@ -82,7 +88,11 @@ async function loadAtlasHistory(projectId: string, scope: "overview" | "build"):
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveAtlasHistory(projectId: string, scope: "overview" | "build", messages: any[]): Promise<void> {
|
|
|
|
|
async function saveAtlasHistory(
|
|
|
|
|
projectId: string,
|
|
|
|
|
scope: "overview" | "build",
|
|
|
|
|
messages: any[],
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await ensureThreadsTable();
|
|
|
|
|
await query(
|
|
|
|
|
@@ -90,7 +100,7 @@ async function saveAtlasHistory(projectId: string, scope: "overview" | "build",
|
|
|
|
|
VALUES ($1, $2, $3::jsonb, NOW())
|
|
|
|
|
ON CONFLICT (project_id, scope) DO UPDATE
|
|
|
|
|
SET messages = $3::jsonb, updated_at = NOW()`,
|
|
|
|
|
[projectId, scope, JSON.stringify(messages)]
|
|
|
|
|
[projectId, scope, JSON.stringify(messages)],
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("[atlas-chat] Failed to save history:", e);
|
|
|
|
|
@@ -104,7 +114,7 @@ async function savePrd(projectId: string, prdContent: string): Promise<void> {
|
|
|
|
|
SET data = data || jsonb_build_object('prd', $2::text, 'stage', 'architecture'),
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = $1`,
|
|
|
|
|
[projectId, prdContent]
|
|
|
|
|
[projectId, prdContent],
|
|
|
|
|
);
|
|
|
|
|
console.log(`[atlas-chat] PRD saved for project ${projectId}`);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
@@ -113,9 +123,14 @@ async function savePrd(projectId: string, prdContent: string): Promise<void> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Replace the latest user message content so DB/UI never show the internal ref prefix. */
|
|
|
|
|
function scrubLastUserMessageContent(history: unknown[], cleanText: string): unknown[] {
|
|
|
|
|
function scrubLastUserMessageContent(
|
|
|
|
|
history: unknown[],
|
|
|
|
|
cleanText: string,
|
|
|
|
|
): unknown[] {
|
|
|
|
|
if (!Array.isArray(history) || history.length === 0) return history;
|
|
|
|
|
const h = history.map(m => (m && typeof m === "object" ? { ...(m as object) } : m));
|
|
|
|
|
const h = history.map((m) =>
|
|
|
|
|
m && typeof m === "object" ? { ...(m as object) } : m,
|
|
|
|
|
);
|
|
|
|
|
for (let i = h.length - 1; i >= 0; i--) {
|
|
|
|
|
const m = h[i] as { role?: string; content?: string };
|
|
|
|
|
if (m?.role === "user" && typeof m.content === "string") {
|
|
|
|
|
@@ -132,7 +147,7 @@ function scrubLastUserMessageContent(history: unknown[], cleanText: string): unk
|
|
|
|
|
|
|
|
|
|
export async function GET(
|
|
|
|
|
req: NextRequest,
|
|
|
|
|
{ params }: { params: Promise<{ projectId: string }> }
|
|
|
|
|
{ params }: { params: Promise<{ projectId: string }> },
|
|
|
|
|
) {
|
|
|
|
|
const session = await authSession();
|
|
|
|
|
if (!session?.user?.email) {
|
|
|
|
|
@@ -146,7 +161,10 @@ export async function GET(
|
|
|
|
|
// 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 }));
|
|
|
|
|
.map((m: any) => ({
|
|
|
|
|
role: m.role as "user" | "assistant",
|
|
|
|
|
content: m.content as string,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return NextResponse.json({ messages });
|
|
|
|
|
}
|
|
|
|
|
@@ -157,7 +175,7 @@ export async function GET(
|
|
|
|
|
|
|
|
|
|
export async function POST(
|
|
|
|
|
req: NextRequest,
|
|
|
|
|
{ params }: { params: Promise<{ projectId: string }> }
|
|
|
|
|
{ params }: { params: Promise<{ projectId: string }> },
|
|
|
|
|
) {
|
|
|
|
|
const session = await authSession();
|
|
|
|
|
if (!session?.user?.email) {
|
|
|
|
|
@@ -180,9 +198,19 @@ export async function POST(
|
|
|
|
|
// 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, scope);
|
|
|
|
|
const history = rawHistory.filter((m: any) =>
|
|
|
|
|
(m.role === "user" || m.role === "assistant") && m.content
|
|
|
|
|
);
|
|
|
|
|
const history = rawHistory
|
|
|
|
|
.filter(
|
|
|
|
|
(m: any) => (m.role === "user" || m.role === "assistant") && m.content,
|
|
|
|
|
)
|
|
|
|
|
.map((m: any) => {
|
|
|
|
|
if (typeof m.content === "string") {
|
|
|
|
|
m.content = m.content
|
|
|
|
|
.replace(/<tool_calls>[\s\S]*?<\/tool_calls>/g, "")
|
|
|
|
|
.replace(/<think>[\s\S]*?<\/think>/g, "")
|
|
|
|
|
.trim();
|
|
|
|
|
}
|
|
|
|
|
return m;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// __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).
|
|
|
|
|
@@ -197,11 +225,13 @@ export async function POST(
|
|
|
|
|
try {
|
|
|
|
|
const rows = await query<{ data: Record<string, unknown> }>(
|
|
|
|
|
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
|
|
|
|
|
[projectId]
|
|
|
|
|
[projectId],
|
|
|
|
|
);
|
|
|
|
|
const kb =
|
|
|
|
|
rows[0]?.data != null
|
|
|
|
|
? formatCreationKickoffForPrompt(rows[0].data as Record<string, unknown>)
|
|
|
|
|
? formatCreationKickoffForPrompt(
|
|
|
|
|
rows[0].data as Record<string, unknown>,
|
|
|
|
|
)
|
|
|
|
|
: null;
|
|
|
|
|
if (kb) {
|
|
|
|
|
kickoffPrefix = `[Project kickoff from creation wizard]\n${kb}\n\n`;
|
|
|
|
|
@@ -236,7 +266,7 @@ export async function POST(
|
|
|
|
|
console.error("[atlas-chat] Agent runner error:", text);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: "Vibn is unavailable. Please try again." },
|
|
|
|
|
{ status: 502 }
|
|
|
|
|
{ status: 502 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -265,7 +295,7 @@ export async function POST(
|
|
|
|
|
console.error("[atlas-chat] Error:", err);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: "Request timed out or failed. Please try again." },
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
{ status: 500 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -276,7 +306,7 @@ export async function POST(
|
|
|
|
|
|
|
|
|
|
export async function DELETE(
|
|
|
|
|
req: NextRequest,
|
|
|
|
|
{ params }: { params: Promise<{ projectId: string }> }
|
|
|
|
|
{ params }: { params: Promise<{ projectId: string }> },
|
|
|
|
|
) {
|
|
|
|
|
const session = await authSession();
|
|
|
|
|
if (!session?.user?.email) {
|
|
|
|
|
@@ -288,21 +318,32 @@ export async function DELETE(
|
|
|
|
|
const sessionId = runnerSessionId(projectId, scope);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
|
|
|
} catch { /* runner may be down */ }
|
|
|
|
|
await fetch(
|
|
|
|
|
`${AGENT_RUNNER_URL}/atlas/sessions/${encodeURIComponent(sessionId)}`,
|
|
|
|
|
{ method: "DELETE" },
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
/* runner may be down */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await ensureThreadsTable();
|
|
|
|
|
await query(
|
|
|
|
|
`DELETE FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`,
|
|
|
|
|
[projectId, scope]
|
|
|
|
|
[projectId, scope],
|
|
|
|
|
);
|
|
|
|
|
} catch { /* table may not exist yet */ }
|
|
|
|
|
} catch {
|
|
|
|
|
/* table may not exist yet */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scope === "overview") {
|
|
|
|
|
try {
|
|
|
|
|
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]);
|
|
|
|
|
} catch { /* legacy */ }
|
|
|
|
|
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [
|
|
|
|
|
projectId,
|
|
|
|
|
]);
|
|
|
|
|
} catch {
|
|
|
|
|
/* legacy */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NextResponse.json({ cleared: true });
|
|
|
|
|
|