diff --git a/app/api/conversations/[id]/messages/route.ts b/app/api/conversations/[id]/messages/route.ts index 7c8f79d..33e4521 100644 --- a/app/api/conversations/[id]/messages/route.ts +++ b/app/api/conversations/[id]/messages/route.ts @@ -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( diff --git a/app/api/ollama/chat/route.ts b/app/api/ollama/chat/route.ts index 1297ba3..83b0238 100644 --- a/app/api/ollama/chat/route.ts +++ b/app/api/ollama/chat/route.ts @@ -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"); } } diff --git a/app/api/ollama/models/route.ts b/app/api/ollama/models/route.ts index 0e71db7..416be55 100644 --- a/app/api/ollama/models/route.ts +++ b/app/api/ollama/models/route.ts @@ -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); } } diff --git a/app/api/ollama/warmup/route.ts b/app/api/ollama/warmup/route.ts new file mode 100644 index 0000000..1c5b43c --- /dev/null +++ b/app/api/ollama/warmup/route.ts @@ -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 }); + } +} + + diff --git a/components/chat/ChatContainer.tsx b/components/chat/ChatContainer.tsx index 1dc5672..0fc0211 100644 --- a/components/chat/ChatContainer.tsx +++ b/components/chat/ChatContainer.tsx @@ -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 diff --git a/database/ollahub.db-shm b/database/ollahub.db-shm index 183738a..14ef20c 100644 Binary files a/database/ollahub.db-shm and b/database/ollahub.db-shm differ diff --git a/database/ollahub.db-wal b/database/ollahub.db-wal index b175a5d..8fd86f8 100644 Binary files a/database/ollahub.db-wal and b/database/ollahub.db-wal differ diff --git a/database/repositories/message.ts b/database/repositories/message.ts index 710e241..d087604 100644 --- a/database/repositories/message.ts +++ b/database/repositories/message.ts @@ -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( diff --git a/lib/ollama.ts b/lib/ollama.ts index ccbb7ea..e2ee3e2 100644 --- a/lib/ollama.ts +++ b/lib/ollama.ts @@ -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