Backend (inference-server/compat.rs): - PreparedPrompt now carries extra_stop_sequences injected by template - Generic template injects User:/System:/Assistant:/Human: stop sequences to prevent the model from continuing the transcript after its reply - Increased repeat_penalty 1.15→1.3 and repeat_last_n 256→512 to break repetition loops more aggressively Backend (inference-server/routes.rs): - Merge template extra_stop_sequences into sampler before inference Frontend (chat.ts): - Added 'System:' and 'Human:' to CHAT_TRANSCRIPT_STOP_SEQUENCES - Extended TRANSCRIPT_LEAKAGE_PATTERN to include 'Human:' Frontend (ChatView.tsx): - buildApiMessages now strips leaked transcript from assistant messages before sending history back to the model - Reduced history window to 20 messages to limit context pollution
35 KiB
Documentação: Frontend Consumindo o Backend - ForgeLM
Sumário
Esta documentação detalha exatamente como o frontend React consome os dados do backend, quais rotas ele bate, como envia e recebe dados, e a arquitetura de comunicação entre as camadas.
Arquitetura de Comunicação
┌─────────────────────────────────────────────────────────────────────────────┐
│ Frontend React + TypeScript │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ App.tsx │ │ ChatView.tsx │ │ useRuntimeManager.ts │ │
│ │ (Main App) │ │ (Chat UI) │ │ (Runtime State) │ │
│ └────────┬────────┘ └────────┬────────┘ └─────────────┬───────────────┘ │
│ │ │ │ │
│ │ │ fetch() │ invoke() │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Camada de API │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ lib/db.ts │ │ features/runtime/api.ts │ │ │
│ │ │ (sql.js + localStorage)│ │ (Tauri Commands) │ │ │
│ │ └─────────────────────┘ └─────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ HTTP / Tauri Bridge
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Tauri Backend (Rust) │ │ HTTP Server (Axum) │ │
│ │ - Commands │ │ - Port: 8080 │ │
│ │ - Events │ │ - /v1/chat/completions │ │
│ │ - Process spawning │ │ - /v1/models │ │
│ └─────────────────────────────┘ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
1. Camadas de Comunicação
1.1 HTTP API (Chat Completions)
Local: ChatView.tsx → HTTP Server Axum
// ChatView.tsx - Linha 251-263
const res = await fetch(`${server}/v1/chat/completions`, {
method: "POST",
headers,
signal: abortRef.current.signal,
body: JSON.stringify({
model,
messages: apiMessages, // Array de {role, content}
stream: useStream, // true/false
stream_options: useStream ? { include_usage: true } : undefined,
max_tokens: 2048,
stop: CHAT_TRANSCRIPT_STOP_SEQUENCES, // ["\nSystem:", "\nUser:", ...]
}),
});
Headers enviados:
const headers: Record<string, string> = {
"Content-Type": "application/json"
};
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
Body da requisição:
{
"model": "qwen2.5-7b-instruct",
"messages": [
{ "role": "user", "content": "Olá, como vai?" }
],
"stream": true,
"stream_options": { "include_usage": true },
"max_tokens": 2048,
"stop": ["\nSystem:", "\nUser:", "\nAssistant:", "\n\nSystem:", "\n\nUser:", "\n\nAssistant:"]
}
1.2 Tauri Commands (Gerenciamento do Runtime)
Local: features/runtime/api.ts → Tauri Backend (Rust)
O frontend usa invoke do @tauri-apps/api/core para chamar comandos Rust:
import { invoke } from "@tauri-apps/api/core";
// Exemplo: Iniciar runtime
export async function startRuntime(): Promise<RuntimeSnapshot> {
return invoke<RuntimeSnapshot>("start_runtime_command");
}
2. Rotas HTTP Consumidas
2.1 POST /v1/chat/completions
Arquivo: ChatView.tsx (linha 251)
Fluxo completo:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Usuário │────►│ ChatView │────►│ fetch() │
│ digita │ │ handleSend │ │ POST /v1/ │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ DB (local) │◄────│ parse() │◄────│ Server │
│ sql.js │ │ response │ │ Axum │
└──────────────┘ └──────────────┘ └──────────────┘
Streaming (SSE):
// ChatView.tsx - Linha 274-307
if (useStream) {
const reader = res.body!.getReader();
const decoder = new TextDecoder('utf-8');
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop()!;
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6).trim();
if (data === "[DONE]") continue;
const chunk = JSON.parse(data);
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) {
fullContent += delta;
setStreamContent(parsed.content);
setStreamReasoning(parsed.reasoning);
}
if (chunk.usage) usage = chunk.usage;
}
}
}
Resposta em streaming:
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Olá"}}]}
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"!"}}]}
...
data: {"id":"...","object":"chat.completion.chunk","choices":[{"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30,"tokens_per_second":45.5}}
data: [DONE]
Modo Blocking:
// ChatView.tsx - Linha 308-318
} else {
const data = await res.json();
fullContent = data.choices?.[0]?.message?.content || "";
usage = data.usage;
}
Resposta blocking:
{
"id": "uuid-123",
"object": "chat.completion",
"created": 1234567890,
"model": "qwen2.5-7b-instruct",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Olá! Como posso ajudar?"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30,
"tokens_per_second": 45.5
}
}
2.2 GET /v1/models
Arquivo: hooks/useModels.ts
const res = await fetch(`${serverUrl}/v1/models`, {
headers: apiKey ? { "Authorization": `Bearer ${apiKey}` } : {},
});
const data = await res.json();
Resposta:
{
"object": "list",
"data": [
{
"id": "local-model",
"object": "model",
"created": 1234567890,
"owned_by": "local"
}
]
}
2.3 GET /health
Arquivo: hooks/useModels.ts (status check)
const res = await fetch(`${serverUrl}/health`);
Resposta:
{
"status": "ok",
"model_loaded": true,
"model_id": "qwen2.5-7b-instruct",
"uptime_secs": 3600
}
3. Comandos Tauri (Invoke)
3.1 Runtime Management API
Arquivo: features/runtime/api.ts
| Função | Comando Tauri | Descrição |
|---|---|---|
bootstrapRuntime() |
bootstrap_runtime_command |
Inicializa o runtime |
getRuntimeSnapshot() |
get_runtime_snapshot_command |
Obtém estado atual |
saveRuntimeConfig(config) |
save_runtime_config_command_handler |
Salva configurações |
startRuntime() |
start_runtime_command |
Inicia o servidor local |
stopRuntime() |
stop_runtime_command |
Para o servidor |
Exemplo - Iniciar Runtime:
// api.ts
export async function startRuntime(): Promise<RuntimeSnapshot> {
try {
return await invoke<RuntimeSnapshot>("start_runtime_command");
} catch {
// Fallback para modo web (sem Tauri)
const snapshot = readFallbackSnapshot();
return writeFallbackSnapshot({
...snapshot,
status: "error",
last_error: {
code: "tauri_unavailable",
message: "Local runtime control requires the Tauri desktop app."
},
});
}
}
3.2 Model Management API
| Função | Comando Tauri | Descrição |
|---|---|---|
scanRuntimeModels(dir) |
scan_models |
Escaneia diretório de modelos |
pickRuntimeModelsDir() |
pick_models_dir |
Abre dialog de seleção |
pickRuntimeModelFiles() |
pick_model_files |
Seleciona arquivos |
swapModel(path) |
swap_model_command |
Troca modelo ativo |
deleteModelFile(path) |
delete_model_file |
Deleta arquivo .gguf |
openModelFolder(path) |
open_model_folder_command |
Abre pasta no Explorer |
Exemplo - Escaneamento:
export async function scanRuntimeModels(dir: string): Promise<RuntimeModelFile[]> {
return invoke<RuntimeModelFile[]>("scan_models", { dir });
}
// Retorna: [{ name: "model.gguf", path: "/path/to/model.gguf", size: 4200000000 }]
3.3 HuggingFace Integration API
| Função | Comando Tauri | Descrição |
|---|---|---|
searchHuggingFaceModels(query, token) |
search_hugging_face_models_command |
Busca modelos |
startHuggingFaceDownload(repo, file, token) |
start_hugging_face_download_command |
Inicia download |
cancelHuggingFaceDownload(id) |
cancel_hugging_face_download_command |
Cancela download |
Exemplo - Busca HuggingFace:
export async function searchHuggingFaceModels(
query: string,
token = ""
): Promise<HuggingFaceModelResult[]> {
return invoke<HuggingFaceModelResult[]>("search_hugging_face_models_command", {
query,
token: token.trim() || null,
});
}
3.4 GPU Info API
export async function queryRuntimeGpu(): Promise<RuntimeGpuInfo | null> {
return invoke<RuntimeGpuInfo>("query_gpu");
}
// Retorna: { name: "NVIDIA RTX 4090", vram_mib: 24576, recommended_layers: 999 }
3.5 ForgeAgent Session API
Arquivo: hooks/useForgeAgentSession.ts
const response = await invoke<StartForgeAgentResponse>(
"start_forgeagent_session_command",
{ prompt }
);
// Retorna: { plan_id: "forgeagent-1234567890", plan: AgentPlan }
Eventos escutados:
listen<StepStartedEvent>("forgeagent:step-started", handler);
listen<StepCompletedEvent>("forgeagent:step-completed", handler);
listen<ArtifactReadyEvent>("forgeagent:artifact-ready", handler);
listen<PlanCompletedEvent>("forgeagent:plan-completed", handler);
listen<SpecialistThinkingStartedEvent>("forgeagent:specialist-thinking-started", handler);
listen<SpecialistThinkingUpdateEvent>("forgeagent:specialist-thinking-update", handler);
listen<SpecialistResponseCompletedEvent>("forgeagent:specialist-response-completed", handler);
4. Persistência Local (sql.js)
4.1 Estrutura do Banco
Arquivo: lib/db.ts
O frontend usa sql.js (SQLite compilado para WebAssembly) com persistência em localStorage:
// initDB() - Criação das tabelas
db.run(`
CREATE TABLE IF NOT EXISTS chats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT 'New Chat',
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
reasoning_content TEXT, -- Conteúdo do <think>...</think>
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
tokens_per_second REAL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS user_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL,
confidence REAL DEFAULT 1.0,
created_at INTEGER,
updated_at INTEGER
)
`);
4.2 Persistência
// db.ts - persist()
persist() {
const data = this.inner.export(); // Uint8Array
// Converte para base64 em chunks
let b64 = "";
const CHUNK = 8192;
for (let i = 0; i < data.length; i += CHUNK) {
b64 += String.fromCharCode(...data.subarray(i, i + CHUNK));
}
const encoded = btoa(b64);
localStorage.setItem("forgelm_db_v2", encoded);
}
4.3 Queries Principais
Carregar mensagens:
// ChatView.tsx - loadMessages()
const rows = db.query<Message>(
"SELECT * FROM messages WHERE chat_id = ? ORDER BY created_at ASC",
[targetChatId]
);
Inserir mensagem do usuário:
db.run(
"INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)",
[cid, text]
);
Inserir resposta do assistente:
db.run(
`INSERT INTO messages (
chat_id, role, content, reasoning_content,
prompt_tokens, completion_tokens, total_tokens, tokens_per_second
) VALUES (?, 'assistant', ?, ?, ?, ?, ?, ?)`,
[cid, content, reasoning, usage?.prompt_tokens, usage?.completion_tokens,
usage?.total_tokens, usage?.tokens_per_second]
);
Histórico para API:
// ChatView.tsx - buildApiMessages()
const history = db.query<{ role: string; content: string }>(
`SELECT role, content FROM messages
WHERE chat_id = ? AND content != ''
AND content NOT LIKE '⚠️ Error:%'
ORDER BY created_at DESC LIMIT ?`,
[cid, 100]
).reverse();
5. Fluxos de Dados Detalhados
5.1 Fluxo: Enviar Mensagem
┌─────────────────────────────────────────────────────────────────────────────┐
│ FLUXO: ENVIAR MENSAGEM │
└─────────────────────────────────────────────────────────────────────────────┘
Usuário clica "Enviar"
│
▼
┌───────────────┐
│ handleSend() │──► 1. Valida input e estado
└───────┬───────┘
│
▼
┌───────────────┐
│ getOrCreateChat│──► 2. Cria novo chat se necessário (INSERT INTO chats)
└───────┬───────┘
│
▼
┌───────────────┐
│ db.run(INSERT) │──► 3. Salva mensagem do usuário na DB local
└───────┬───────┘
│
▼
┌───────────────┐
│ buildApiMessages│──► 4. Busca histórico das últimas 100 mensagens
└───────┬───────┘
│
▼
┌───────────────────────────┐
│ fetch(POST /v1/chat/completions) │──► 5. Envia para servidor HTTP
│ │ Headers: Content-Type, Authorization
│ │ Body: {model, messages, stream, ...}
└───────────┬───────────────┘
│
┌────┴────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Stream │ │ Block │
│ Mode │ │ Mode │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌───────────────────────────┐
│ Processa chunks SSE │
│ - setStreamContent() │
│ - setStreamReasoning() │
│ - parseAssistantResponse()│
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ db.run(INSERT assistant) │──► 6. Salva resposta completa na DB
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ loadMessages() │──► 7. Recarrega mensagens do chat
└───────────────────────────┘
5.2 Fluxo: Trocar Modelo
Usuário seleciona novo modelo no dropdown
│
▼
┌───────────────────────────┐
│ onModelChange() │
│ onModelSwap() │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ swapModel(newModelPath) │──► invoke("swap_model_command")
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Tauri Backend (Rust) │
│ 1. stop_runtime() │
│ 2. Atualiza model_path │
│ 3. save_runtime_config() │
│ 4. start_runtime() │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ RuntimeSnapshot retornado │
│ { status: "ready", ... } │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ setSelectedModel(path) │──► Atualiza estado React
└───────────────────────────┘
5.3 Fluxo: Download HuggingFace
Usuário busca e seleciona modelo
│
▼
┌───────────────────────────────┐
│ searchHuggingFaceModels() │──► invoke("search_hugging_face_models_command")
│ │ Args: { query, token }
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ Lista de modelos retornada │
│ [{ repo_id, author, │
│ verified_files: [...] }] │
└───────────────┬───────────────┘
│
▼
Usuário clica "Download"
│
▼
┌───────────────────────────────┐
│ startHuggingFaceDownload() │──► invoke("start_hugging_face_download_command")
│ │ Args: { repo, fileName, token }
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ Escuta eventos de progresso │
│ listen("hugging-face-download- │
│ progress", handler) │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ Eventos recebidos: │
│ { phase: "progress", │
│ progressPercentage: 45, │
│ downloadedMB: 2048, │
│ totalMB: 4096 } │
└───────────────────────────────┘
5.4 Fluxo: ForgeAgent
Usuário ativa modo Agentic e envia mensagem
│
▼
┌───────────────────────────────┐
│ start_forgeagent_session │──► invoke("start_forgeagent_session_command")
│ _command({ prompt }) │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ Resposta inicial: │
│ { plan_id, plan: { │
│ title, overview, steps[] }} │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ Escuta eventos Tauri: │
├───────────────────────────────┤
│ forgeagent:step-started │──► Atualiza UI com passo atual
│ forgeagent:step-completed │──► Marca passo como concluído
│ forgeagent:specialist- │──► Mostra especialista
│ thinking-started │
│ forgeagent:specialist- │──► Atualiza pensamento
│ thinking-update │
│ forgeagent:specialist- │──► Mostra resposta do especialista
│ response-completed │
│ forgeagent:plan-completed │──► Mostra resumo final
└───────────────────────────────┘
6. Hooks Principais
6.1 useRuntimeManager
Arquivo: features/runtime/useRuntimeManager.ts
Gerencia todo o estado do runtime local:
export function useRuntimeManager() {
// Estados
const [draftState, setDraftState] = useState<RuntimeDraftState>(...);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
// Métodos expostos
return {
snapshot: draftState.liveSnapshot, // Estado atual do runtime
connection: { mode, serverUrl, apiKey }, // Dados de conexão
startLocal, // Inicia servidor
stopLocal, // Para servidor
saveConfig, // Salva configurações
scanModels, // Escaneia modelos
// ... e mais
};
}
Polling: Atualiza a cada 3 segundos
const id = window.setInterval(() => {
refresh().catch(() => undefined);
}, 3000);
6.2 useModels
Arquivo: hooks/useModels.ts
Gerencia a lista de modelos disponíveis:
export function useModels(serverUrl: string, apiKey: string, enabled: boolean) {
const [models, setModels] = useState<ModelInfo[]>([]);
const [selectedModel, setSelectedModel] = useState("");
const [status, setStatus] = useState<ServerStatus>("loading");
const refresh = useCallback(async () => {
// 1. Health check
const healthRes = await fetch(`${serverUrl}/health`);
// 2. Lista modelos
const modelsRes = await fetch(`${serverUrl}/v1/models`, { headers });
const data = await modelsRes.json();
setModels(data.data);
}, [serverUrl, apiKey]);
return { models, selectedModel, setSelectedModel, status, refresh };
}
6.3 useForgeAgentSession
Arquivo: hooks/useForgeAgentSession.ts
export function useForgeAgentSession() {
const [state, setState] = useState<AgentPlanState | null>(null);
const start = useCallback(async (prompt: string) => {
const response = await invoke<StartForgeAgentResponse>(
"start_forgeagent_session_command",
{ prompt }
);
// Inicializa estado com plano recebido
const initial = createForgeAgentState(response.plan_id, response.plan);
updateState(initial);
}, []);
// Escuta eventos em tempo real
useEffect(() => {
const unlisten = await Promise.all([
listen<StepStartedEvent>("forgeagent:step-started", handler),
listen<StepCompletedEvent>("forgeagent:step-completed", handler),
// ... mais eventos
]);
}, []);
return { state, start, reset };
}
6.4 useDB
Arquivo: hooks/useDB.ts
export function useDB() {
const [db, setDb] = useState<DB | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
initDB().then((database) => {
setDb(database);
setReady(true);
});
}, []);
return { db, ready };
}
7. Tipos de Dados (TypeScript)
7.1 Mensagens
// lib/types.ts
interface Message {
id: number;
chat_id: number;
role: "user" | "assistant" | "system";
content: string;
reasoning_content?: string | null; // <think>...</think>
display_mode?: "default" | "agentic_final";
forgeagent_state?: AgentPlanState;
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
tokens_per_second?: number;
created_at: number;
}
7.2 Runtime
// features/runtime/types.ts
interface RuntimeSnapshot {
status: "stopped" | "starting" | "loading" | "ready" | "degraded" | "stopping" | "error";
ready: boolean;
process_running: boolean;
resolved_url: string;
binary_path: string | null;
last_error: { code: string; message: string } | null;
active_command: "idle" | "save" | "start" | "stop" | "save_and_start";
last_command_result: {
command: string;
outcome: "succeeded" | "blocked" | "failed";
message: string;
};
config: RuntimeConfig;
}
interface RuntimeConfig {
connection_mode: "local" | "remote";
server_url: string;
api_key: string;
model_path: string | null;
models_dir: string | null;
addr: string; // "127.0.0.1:8080"
gpu_layers: number;
ctx_size: number;
// ...
}
7.3 ForgeAgent
// types/forgeagent.ts
interface AgentPlan {
title: string;
overview: string;
steps: AgentStep[];
}
interface AgentStep {
id: number;
title: string;
description: string;
status: "pending" | "running" | "completed" | "error";
sub_steps: AgentSubStep[];
}
interface SpecialistResponse {
specialist_id: string;
specialist_name: string;
thinking: string;
response: string;
}
8. Resumo das Comunicações
Tabela de Endpoints
| Origem | Destino | Método | Rota/Command | Payload | Response |
|---|---|---|---|---|---|
| ChatView | HTTP Server | POST | /v1/chat/completions |
{model, messages, stream, max_tokens} |
SSE ou JSON |
| useModels | HTTP Server | GET | /v1/models |
- | {data: ModelInfo[]} |
| useModels | HTTP Server | GET | /health |
- | {status, model_loaded} |
| useRuntimeManager | Tauri | invoke | start_runtime_command |
- | RuntimeSnapshot |
| useRuntimeManager | Tauri | invoke | stop_runtime_command |
- | RuntimeSnapshot |
| useRuntimeManager | Tauri | invoke | save_runtime_config_command_handler |
{config} |
RuntimeSnapshot |
| useRuntimeManager | Tauri | invoke | scan_models |
{dir} |
RuntimeModelFile[] |
| useRuntimeManager | Tauri | invoke | swap_model_command |
{newModelPath} |
RuntimeSnapshot |
| API HuggingFace | Tauri | invoke | search_hugging_face_models_command |
{query, token} |
HuggingFaceModelResult[] |
| API HuggingFace | Tauri | invoke | start_hugging_face_download_command |
{repo, fileName, token} |
downloadId |
| useForgeAgentSession | Tauri | invoke | start_forgeagent_session_command |
{prompt} |
{plan_id, plan} |
| useForgeAgentSession | Tauri | listen | forgeagent:step-started |
- | StepStartedEvent |
| useForgeAgentSession | Tauri | listen | forgeagent:step-completed |
- | StepCompletedEvent |
| useForgeAgentSession | Tauri | listen | forgeagent:plan-completed |
- | PlanCompletedEvent |
9. Configurações e Estado Global
Estado do Runtime (Contexto Global)
// App.tsx
const runtime = useRuntimeManager();
// Acesso em componentes filhos via props
<ChatView
server={runtime.connection.serverUrl} // "http://127.0.0.1:8080"
apiKey={runtime.connection.apiKey} // ""
model={selectedModel} // "qwen2.5-7b-instruct"
runtimeStatus={runtime.snapshot.status} // "ready"
/>
Persistência do Runtime
// localStorage: forgelm_runtime_snapshot_v1
{
"status": "ready",
"ready": true,
"process_running": true,
"resolved_url": "http://127.0.0.1:8080",
"config": {
"connection_mode": "local",
"model_path": "D:/models/model.gguf",
"addr": "127.0.0.1:8080"
}
}
10. Diagrama de Sequência - Chat Completo
Usuário ChatView DB(sql.js) HTTP Server Tauri Modelo
│ │ │ │ │ │
│──mensagem►│ │ │ │ │
│ │──INSERT───►│ │ │ │
│ │◄───────────│ │ │ │
│ │ │ │ │ │
│ │──SELECT───►│ (histórico) │ │ │
│ │◄───────────│ │ │ │
│ │ │ │ │ │
│ │────────────┼──────────────┼──────────┼──► │
│ │ fetch() │ │ │ inference
│ │ POST /v1/ │ │ │ │
│ │chat/completions │ │ │
│ │ │ │ │ │
│ │◄───────────┼──────────────┼──────────┼─────────│
│ │ SSE │ │ │ │
│ │ chunks │ │ │ │
│ │ │ │ │ │
│◄──stream─│ │ │ │ │
│ tokens │ │ │ │ │
│ │ │ │ │ │
│ │──INSERT───►│ │ │ │
│ │ assistant │ │ │ │
│ │◄───────────│ │ │ │
│ │ │ │ │ │
│◄─display │ │ │ │ │
│ completo│ │ │ │ │
Conclusão
O frontend ForgeLM consome o backend através de duas camadas distintas:
- HTTP API - Para chat completions (streaming e blocking) via
fetch() - Tauri Commands - Para gerenciamento do runtime, modelos e downloads via
invoke()
A persistência local usa sql.js (SQLite WASM) com backup em localStorage, garantindo que conversas e memórias do usuário sejam mantidas entre sessões.
O sistema suporta dois modos:
- Local: Tauri gerencia o processo do servidor HTTP internamente
- Remote: Conecta a um servidor OpenAI-compatible externo
Todos os comandos Tauri possuem fallbacks para modo web, permitindo que o app funcione (com funcionalidade reduzida) mesmo sem o backend Tauri.