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:
1 parent
30290ae87a
commit
1545020afe
9 files changed
+114
-38
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user