feat(api): add pagination to messages endpoint and improve chat API robustness

- Add pagination support to /api/conversations/[id]/messages with limit and before query params, returning hasMore and cursor for efficient message fetching
- Enhance /api/ollama/chat by adding non-blocking Ollama availability check and conditionally disabling debug logging in production环境下Featured
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-29 17:22:21 -03:00
1 parent 30290ae87a
commit 1545020afe
9 files changed
+114 -38

No files matched your search

+11 -2
View File
@@ -10,12 +10,21 @@ export async function GET(
) {
try {
const { id } = await params;
const url = new URL(request.url);
const limitParam = url.searchParams.get("limit");
const beforeParam = url.searchParams.get("before");
const limit = Math.min(Math.max(Number(limitParam) || 0, 1), 200) || 50;
const beforeTs = beforeParam ? Number(beforeParam) : undefined;
const db = getDatabase();
const messageRepo = new MessageRepository(db);
const messages = messageRepo.findByConversation(id);
const messages = messageRepo.findByConversationPaged(id, limit, beforeTs);
const hasMore = messages.length === limit;
const cursor = messages.length > 0 ? messages[0].timestamp : null;
return NextResponse.json(messages);
return NextResponse.json({ messages, hasMore, cursor });
} catch (error) {
console.error("Erro ao buscar mensagens:", error);
return NextResponse.json(
+39 -33
View File
@@ -1,5 +1,5 @@
import { NextRequest } from "next/server";
import { chatWithStream } from "@/lib/ollama";
import { chatWithStream, ensureOllamaAvailable } from "@/lib/ollama";
import { convertToOllamaMessages } from "@/lib/chat";
import { detectModelCapabilities } from "@/lib/services/model-capabilities";
import { MCPExecutor } from "@/lib/services/mcp-executor";
@@ -97,24 +97,30 @@ export async function POST(req: NextRequest) {
};
try {
// Garantir disponibilidade com timeout curto (não bloquear)
ensureOllamaAvailable({ timeoutMs: 2000 }).catch(() => {});
const ollamaMessages = convertToOllamaMessages(messages);
console.log("🗣️ [CHAT] Messages ->", ollamaMessages);
if (process.env.NODE_ENV !== "production") {
console.log("🗣️ [CHAT] Messages ->", ollamaMessages);
}
// Merge system prompt com tools prompt
const finalSystemPrompt = toolsPrompt
? `${system || ""}\n\n${toolsPrompt}`
: system;
if (toolsPrompt) console.log("🧩 [CHAT] Tools prompt injected");
if (toolsPrompt && process.env.NODE_ENV !== "production") console.log("🧩 [CHAT] Tools prompt injected");
// Preparar parâmetros para chat
const chatParams = capabilities.supportsNativeTools ? { tools } : {};
if (capabilities.supportsNativeTools) {
console.log(
"🧰 [CHAT] Native tools enabled:",
tools?.map((t: any) => t.function?.name)
);
} else {
console.log("🧰 [CHAT] Prompt-engineering tools mode");
if (process.env.NODE_ENV !== "production") {
if (capabilities.supportsNativeTools) {
console.log(
"🧰 [CHAT] Native tools enabled:",
tools?.map((t: any) => t.function?.name)
);
} else {
console.log("🧰 [CHAT] Prompt-engineering tools mode");
}
}
const response = await chatWithStream(
@@ -124,7 +130,7 @@ export async function POST(req: NextRequest) {
finalSystemPrompt,
capabilities.supportsNativeTools ? tools : undefined
);
console.log("🌊 [CHAT] Streaming started for model:", model);
if (process.env.NODE_ENV !== "production") console.log("🌊 [CHAT] Streaming started for model:", model);
const normalizeParameters = (params: any) => {
try {
@@ -149,7 +155,7 @@ export async function POST(req: NextRequest) {
};
for await (const chunk of response) {
console.log("📦 [CHAT stream chunk]", JSON.stringify(chunk));
if (process.env.NODE_ENV !== "production") console.log("📦 [CHAT stream chunk]", JSON.stringify(chunk));
// Verificar se há tool calls (modelos com suporte nativo)
if (
(chunk.message?.tool_calls &&
@@ -157,19 +163,16 @@ export async function POST(req: NextRequest) {
(Array.isArray((chunk as any).tool_calls) &&
(chunk as any).tool_calls.length > 0)
) {
console.log("🔧 Model requested tool calls (native)");
if (process.env.NODE_ENV !== "production") console.log("🔧 Model requested tool calls (native)");
const nativeCalls =
chunk.message?.tool_calls || (chunk as any).tool_calls || [];
for (const toolCall of nativeCalls) {
const toolKey = toolCall.function.name;
const toolInfo = mcpToolsMap.get(toolKey);
console.log(
"🧭 [CHAT] Native tool call:",
toolKey,
"->",
toolInfo
);
if (process.env.NODE_ENV !== "production") {
console.log("🧭 [CHAT] Native tool call:", toolKey, "->", toolInfo);
}
if (toolInfo) {
// Executar tool
@@ -185,11 +188,13 @@ export async function POST(req: NextRequest) {
if (result.success) {
toolExecuted = true;
console.log("✅ [CHAT] Tool executed successfully", {
mcpId: toolInfo.mcpId,
toolName: toolInfo.toolName,
executionTime: result.executionTime,
});
if (process.env.NODE_ENV !== "production") {
console.log("✅ [CHAT] Tool executed successfully", {
mcpId: toolInfo.mcpId,
toolName: toolInfo.toolName,
executionTime: result.executionTime,
});
}
// Enviar indicador de execução de tool
safeEnqueue(
JSON.stringify({
@@ -206,10 +211,12 @@ export async function POST(req: NextRequest) {
toolInfo.toolName,
result.result
);
console.log(
"🧩 [CHAT] Injecting tool result into context (length)",
String(resultMessage).length
);
if (process.env.NODE_ENV !== "production") {
console.log(
"🧩 [CHAT] Injecting tool result into context (length)",
String(resultMessage).length
);
}
// Fazer nova chamada ao modelo com o resultado
const updatedMessages = [
@@ -264,9 +271,9 @@ export async function POST(req: NextRequest) {
extractToolCallFromResponse(accumulatedResponse);
if (extractedToolCall) {
console.log("🔧 Model requested tool call (prompt engineering)");
if (process.env.NODE_ENV !== "production") console.log("🔧 Model requested tool call (prompt engineering)");
lastExtractedToolCall = extractedToolCall;
console.log("🧭 [CHAT] Extracted tool call:", extractedToolCall);
if (process.env.NODE_ENV !== "production") console.log("🧭 [CHAT] Extracted tool call:", extractedToolCall);
// Encontrar MCP e tool
let foundToolInfo: { mcpId: string; toolName: string } | null =
@@ -339,9 +346,8 @@ export async function POST(req: NextRequest) {
}
}
} else {
console.log(
"🕵️ [CHAT] No tool call extracted from accumulated response"
);
if (process.env.NODE_ENV !== "production")
console.log("🕵️ [CHAT] No tool call extracted from accumulated response");
}
}
+15 -2
View File
@@ -10,6 +10,10 @@ import {
export const dynamic = "force-dynamic";
// Cache leve em memória para reduzir chamadas repetidas em curto intervalo
let __modelsCache: { ts: number; payload: any } | null = null;
const CACHE_TTL_MS = 8000; // 8s
function normalizeModel(model: any): ModelInfo {
const name: string = model?.model || model?.name || "unknown";
const sizeBytes: number = model?.size || 0;
@@ -30,6 +34,11 @@ function normalizeModel(model: any): ModelInfo {
export async function GET() {
try {
// Tentar retorno via cache recente
if (__modelsCache && Date.now() - __modelsCache.ts < CACHE_TTL_MS) {
return NextResponse.json(__modelsCache.payload);
}
let models: any[] = [];
// Tentar garantir disponibilidade rapidamente (não bloquear longamente a UI)
await ensureOllamaAvailable({ timeoutMs: 3000 }).catch(() => {});
@@ -39,8 +48,12 @@ export async function GET() {
models = await listModelsViaHttp();
}
const normalized = models.map((m) => normalizeModel(m));
return NextResponse.json({ offline: false, models: normalized });
const payload = { offline: false, models: normalized };
__modelsCache = { ts: Date.now(), payload };
return NextResponse.json(payload);
} catch (e) {
return NextResponse.json({ offline: true, models: mockModels });
const payload = { offline: true, models: mockModels };
__modelsCache = { ts: Date.now(), payload };
return NextResponse.json(payload);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
import { Ollama } from "ollama";
import { OLLAMA_HOST } from "@/lib/ollama";
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
try {
const url = new URL(req.url);
const model = url.searchParams.get("model");
if (!model) {
return NextResponse.json({ error: "model required" }, { status: 400 });
}
const client = new Ollama({ host: OLLAMA_HOST });
await client.chat({
model,
messages: [{ role: "system", content: "warm" }],
stream: false,
options: { keep_alive: "5m" },
});
return NextResponse.json({ warmed: true });
} catch (e) {
return NextResponse.json({ warmed: false }, { status: 200 });
}
}
+4
View File
@@ -46,6 +46,8 @@ export function ChatContainer({
useEffect(() => {
if (ready && prefs.selectedModel && !selectedModel) {
setSelectedModel(prefs.selectedModel);
// Warmup do modelo em background
fetch(`/api/ollama/warmup?model=${encodeURIComponent(prefs.selectedModel)}`).catch(() => {});
}
}, [ready, prefs.selectedModel]);
@@ -64,6 +66,8 @@ export function ChatContainer({
const handleModelChange = (modelName: string) => {
setSelectedModel(modelName);
update({ selectedModel: modelName }); // Salvar no Redis
// Disparar warmup em background
fetch(`/api/ollama/warmup?model=${encodeURIComponent(modelName)}`).catch(() => {});
};
// Carregar MCPs ativos do localStorage
Binary file not shown.
Binary file not shown.
+15
View File
@@ -39,6 +39,21 @@ export class MessageRepository {
.all(conversationId) as Message[];
}
findByConversationPaged(
conversationId: string,
limit: number,
beforeTs?: number
): Message[] {
const sql = beforeTs
? `SELECT * FROM messages WHERE conversation_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?`
: `SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?`;
const rows = beforeTs
? (this.db.prepare(sql).all(conversationId, beforeTs, limit) as Message[])
: (this.db.prepare(sql).all(conversationId, limit) as Message[]);
// Reverter para ascendente no retorno
return rows.reverse();
}
findById(id: string): Message | null {
return this.db
.prepare(
+1 -1
View File
@@ -83,7 +83,7 @@ export async function chatWithStream(
? [{ role: "system", content: system }, ...messages]
: messages,
stream: true,
options,
options: { ...(options || {}), keep_alive: "5m" },
};
// Adicionar tools se fornecidos