fix: stop transcript leakage and repetition loops in Generic chat template
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
This commit is contained in:
1 parent
5f86c69b03
commit
88f4319c3f
6 files changed
+1951
-7
No files matched your search
@@ -0,0 +1,962 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
```
|
||||
|
||||
**Body da requisição:**
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```typescript
|
||||
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):**
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
|
||||
```typescript
|
||||
// ChatView.tsx - Linha 308-318
|
||||
} else {
|
||||
const data = await res.json();
|
||||
fullContent = data.choices?.[0]?.message?.content || "";
|
||||
usage = data.usage;
|
||||
}
|
||||
```
|
||||
|
||||
**Resposta blocking:**
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
```typescript
|
||||
const res = await fetch(`${serverUrl}/v1/models`, {
|
||||
headers: apiKey ? { "Authorization": `Bearer ${apiKey}` } : {},
|
||||
});
|
||||
const data = await res.json();
|
||||
```
|
||||
|
||||
**Resposta:**
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "local-model",
|
||||
"object": "model",
|
||||
"created": 1234567890,
|
||||
"owned_by": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 GET /health
|
||||
|
||||
**Arquivo:** `hooks/useModels.ts` (status check)
|
||||
|
||||
```typescript
|
||||
const res = await fetch(`${serverUrl}/health`);
|
||||
```
|
||||
|
||||
**Resposta:**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
const response = await invoke<StartForgeAgentResponse>(
|
||||
"start_forgeagent_session_command",
|
||||
{ prompt }
|
||||
);
|
||||
// Retorna: { plan_id: "forgeagent-1234567890", plan: AgentPlan }
|
||||
```
|
||||
|
||||
**Eventos escutados:**
|
||||
```typescript
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
db.run(
|
||||
"INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)",
|
||||
[cid, text]
|
||||
);
|
||||
```
|
||||
|
||||
**Inserir resposta do assistente:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
const id = window.setInterval(() => {
|
||||
refresh().catch(() => undefined);
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
### 6.2 useModels
|
||||
|
||||
**Arquivo:** `hooks/useModels.ts`
|
||||
|
||||
Gerencia a lista de modelos disponíveis:
|
||||
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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)
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
1. **HTTP API** - Para chat completions (streaming e blocking) via `fetch()`
|
||||
2. **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.
|
||||
Reference in new issue
Block a user