feat(chat): implement adaptive timeout and enhanced search functionality
- Introduced adaptive timeout logic for web searches and chat streaming, improving responsiveness and efficiency. - Updated `smartSearchRag` method to support timeout adjustments based on search rounds. - Enhanced `executeProgressiveSearch` to utilize adaptive timeouts and improve search result handling. - Added new utility functions for calculating adaptive timeouts based on rounds and attempts. - Integrated event listeners for chat session management, improving user experience during chat interactions. - Refactored chat message handling to accommodate new streaming and timeout features.
This commit is contained in:
1 parent
f2e8a1b933
commit
8c546f6474
13 files changed
+1547
-271
No files matched your search
@@ -0,0 +1,308 @@
|
||||
use rusqlite::{Connection, Result as SqliteResult, params};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChatSession {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub emoji: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChatMessage {
|
||||
pub id: Option<i64>,
|
||||
pub session_id: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub metadata: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Cria ou abre conexão com o banco de dados
|
||||
pub fn new(app_handle: &AppHandle) -> SqliteResult<Self> {
|
||||
let app_data_dir = app_handle.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::SqliteFailure(
|
||||
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
|
||||
Some(format!("Failed to get app data dir: {}", e))
|
||||
)
|
||||
})?;
|
||||
|
||||
std::fs::create_dir_all(&app_data_dir)
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::SqliteFailure(
|
||||
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
|
||||
Some(format!("Failed to create app data dir: {}", e))
|
||||
)
|
||||
})?;
|
||||
|
||||
let db_path = app_data_dir.join("ollahub.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
let db = Self { conn };
|
||||
db.init_schema()?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Inicializa o schema do banco de dados
|
||||
fn init_schema(&self) -> SqliteResult<()> {
|
||||
// Tabela de sessões
|
||||
self.conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
emoji TEXT DEFAULT '💬',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Tabela de mensagens
|
||||
self.conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
metadata TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Tabela de documentos RAG
|
||||
self.conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS rag_documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT,
|
||||
source_url TEXT,
|
||||
content TEXT NOT NULL,
|
||||
embedding BLOB,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Índices para performance
|
||||
self.conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
self.conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_rag_session_id ON rag_documents(session_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cria uma nova sessão de chat
|
||||
pub fn create_session(&self, session: &ChatSession) -> SqliteResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO sessions (id, title, emoji, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = ?2,
|
||||
emoji = ?3,
|
||||
updated_at = ?5",
|
||||
params![
|
||||
session.id,
|
||||
session.title,
|
||||
session.emoji,
|
||||
session.created_at.to_rfc3339(),
|
||||
session.updated_at.to_rfc3339()
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Atualiza uma sessão existente
|
||||
pub fn update_session(&self, session: &ChatSession) -> SqliteResult<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE sessions SET title = ?1, emoji = ?2, updated_at = ?3 WHERE id = ?4",
|
||||
params![
|
||||
session.title,
|
||||
session.emoji,
|
||||
session.updated_at.to_rfc3339(),
|
||||
session.id
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Busca uma sessão por ID
|
||||
pub fn get_session(&self, session_id: &str) -> SqliteResult<Option<ChatSession>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, title, emoji, created_at, updated_at FROM sessions WHERE id = ?1"
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query_map(params![session_id], |row| {
|
||||
Ok(ChatSession {
|
||||
id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
emoji: row.get(2)?,
|
||||
created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(3)?)
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(3, "TEXT".to_string(), rusqlite::types::Type::Text))?
|
||||
.with_timezone(&Utc),
|
||||
updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(4)?)
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(4, "TEXT".to_string(), rusqlite::types::Type::Text))?
|
||||
.with_timezone(&Utc),
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(row) = rows.next() {
|
||||
row.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lista todas as sessões ordenadas por updated_at DESC
|
||||
pub fn list_sessions(&self) -> SqliteResult<Vec<ChatSession>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, title, emoji, created_at, updated_at FROM sessions ORDER BY updated_at DESC"
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(ChatSession {
|
||||
id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
emoji: row.get(2)?,
|
||||
created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(3)?)
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(3, "TEXT".to_string(), rusqlite::types::Type::Text))?
|
||||
.with_timezone(&Utc),
|
||||
updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(4)?)
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(4, "TEXT".to_string(), rusqlite::types::Type::Text))?
|
||||
.with_timezone(&Utc),
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
for row in rows {
|
||||
sessions.push(row?);
|
||||
}
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
/// Deleta uma sessão e todas as suas mensagens
|
||||
pub fn delete_session(&self, session_id: &str) -> SqliteResult<()> {
|
||||
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![session_id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adiciona uma mensagem a uma sessão
|
||||
pub fn add_message(&self, message: &ChatMessage) -> SqliteResult<i64> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO messages (session_id, role, content, metadata, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![
|
||||
message.session_id,
|
||||
message.role,
|
||||
message.content,
|
||||
message.metadata,
|
||||
message.created_at.to_rfc3339()
|
||||
],
|
||||
)?;
|
||||
|
||||
// Atualizar updated_at da sessão
|
||||
self.conn.execute(
|
||||
"UPDATE sessions SET updated_at = ?1 WHERE id = ?2",
|
||||
params![
|
||||
message.created_at.to_rfc3339(),
|
||||
message.session_id
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Busca todas as mensagens de uma sessão
|
||||
pub fn get_messages(&self, session_id: &str) -> SqliteResult<Vec<ChatMessage>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, session_id, role, content, metadata, created_at
|
||||
FROM messages
|
||||
WHERE session_id = ?1
|
||||
ORDER BY created_at ASC"
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map(params![session_id], |row| {
|
||||
Ok(ChatMessage {
|
||||
id: Some(row.get(0)?),
|
||||
session_id: row.get(1)?,
|
||||
role: row.get(2)?,
|
||||
content: row.get(3)?,
|
||||
metadata: row.get(4)?,
|
||||
created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(5)?)
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(5, "TEXT".to_string(), rusqlite::types::Type::Text))?
|
||||
.with_timezone(&Utc),
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for row in rows {
|
||||
messages.push(row?);
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Salva um documento RAG
|
||||
pub fn save_rag_document(
|
||||
&self,
|
||||
id: &str,
|
||||
session_id: Option<&str>,
|
||||
source_url: Option<&str>,
|
||||
content: &str,
|
||||
embedding: Option<&[u8]>,
|
||||
) -> SqliteResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO rag_documents (id, session_id, source_url, content, embedding, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
id,
|
||||
session_id,
|
||||
source_url,
|
||||
content,
|
||||
embedding,
|
||||
Utc::now().to_rfc3339()
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Busca documentos RAG por sessão
|
||||
pub fn get_rag_documents(&self, session_id: &str) -> SqliteResult<Vec<(String, String, Option<String>)>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, content, source_url FROM rag_documents WHERE session_id = ?1"
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map(params![session_id], |row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut docs = Vec::new();
|
||||
for row in rows {
|
||||
docs.push(row?);
|
||||
}
|
||||
Ok(docs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ mod scheduler_loop;
|
||||
mod sources_config;
|
||||
mod system_monitor;
|
||||
mod intent_classifier;
|
||||
mod db;
|
||||
|
||||
use web_scraper::{
|
||||
ScrapedContent,
|
||||
@@ -50,6 +51,27 @@ struct Message {
|
||||
metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// Eventos para comunicação Frontend <-> Rust
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
struct ChatCreatedEvent {
|
||||
session_id: String,
|
||||
title: String,
|
||||
emoji: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
struct ChatTokenEvent {
|
||||
session_id: String,
|
||||
content: String,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
struct ChatErrorEvent {
|
||||
session_id: String,
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct DownloadProgress {
|
||||
status: String, // "pulling", "verifying", "success"
|
||||
@@ -2599,6 +2621,272 @@ fn classify_intent(query: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Comando principal para streaming de chat via Rust
|
||||
#[command]
|
||||
async fn chat_stream(
|
||||
window: Window,
|
||||
app_handle: AppHandle,
|
||||
session_id: Option<String>,
|
||||
messages: Vec<Message>,
|
||||
model: String,
|
||||
system_prompt: Option<String>,
|
||||
enable_rag: Option<bool>,
|
||||
) -> Result<String, String> {
|
||||
use uuid::Uuid;
|
||||
use ollama_client::OllamaClient;
|
||||
use futures_util::StreamExt;
|
||||
use db::{Database, ChatSession, ChatMessage};
|
||||
|
||||
// Gerar ou usar session_id existente
|
||||
let session_id = session_id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
let enable_rag = enable_rag.unwrap_or(false);
|
||||
|
||||
// Verificar se é nova sessão (apenas 1 mensagem do usuário)
|
||||
let is_new_session = messages.len() == 1 && messages[0].role == "user";
|
||||
|
||||
// Variáveis para título e emoji (usadas depois na persistência)
|
||||
let (title, emoji) = if is_new_session {
|
||||
let user_input = &messages[0].content;
|
||||
let ollama_client = OllamaClient::new(None);
|
||||
|
||||
// Tentar gerar título (com timeout curto)
|
||||
let generated_title = match tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(5),
|
||||
ollama_client.generate_title(&model, user_input)
|
||||
).await {
|
||||
Ok(Ok(t)) => t,
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("Erro ao gerar título: {}. Usando fallback.", e);
|
||||
// Fallback: primeiras palavras da pergunta
|
||||
user_input.split_whitespace().take(5).collect::<Vec<_>>().join(" ")
|
||||
},
|
||||
Err(_) => {
|
||||
log::warn!("Timeout ao gerar título. Usando fallback.");
|
||||
user_input.split_whitespace().take(5).collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
};
|
||||
|
||||
let generated_emoji = OllamaClient::generate_emoji(&generated_title);
|
||||
|
||||
// Emitir evento de chat criado
|
||||
let created_event = ChatCreatedEvent {
|
||||
session_id: session_id.clone(),
|
||||
title: generated_title.clone(),
|
||||
emoji: generated_emoji.clone(),
|
||||
};
|
||||
|
||||
if let Err(e) = window.emit("chat-created", &created_event) {
|
||||
log::warn!("Erro ao emitir evento chat-created: {}", e);
|
||||
}
|
||||
|
||||
(generated_title, generated_emoji)
|
||||
} else {
|
||||
(String::new(), "💬".to_string())
|
||||
};
|
||||
|
||||
// 2. Preparar mensagens para Ollama
|
||||
let mut ollama_messages = Vec::new();
|
||||
|
||||
// Adicionar system prompt se fornecido
|
||||
if let Some(sys_prompt) = system_prompt {
|
||||
ollama_messages.push(serde_json::json!({
|
||||
"role": "system",
|
||||
"content": sys_prompt
|
||||
}));
|
||||
}
|
||||
|
||||
// Converter mensagens para formato Ollama
|
||||
for msg in &messages {
|
||||
ollama_messages.push(serde_json::json!({
|
||||
"role": msg.role,
|
||||
"content": msg.content
|
||||
}));
|
||||
}
|
||||
|
||||
// 3. TODO: Classificar intent e aplicar RAG se necessário
|
||||
// if enable_rag {
|
||||
// let intent = classify_intent(messages.last().unwrap().content.clone());
|
||||
// // Buscar contexto via RAG
|
||||
// // Injetar no system prompt
|
||||
// }
|
||||
|
||||
// 4. Fazer requisição streaming para Ollama
|
||||
let ollama_client = OllamaClient::new(None);
|
||||
ollama_client.check_connection().await?;
|
||||
|
||||
let request = serde_json::json!({
|
||||
"model": model,
|
||||
"messages": ollama_messages,
|
||||
"stream": true
|
||||
});
|
||||
|
||||
// Usar reqwest diretamente para streaming
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
let url = "http://localhost:11434/api/chat";
|
||||
let response = client
|
||||
.post(url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request to Ollama: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_msg = format!("Ollama returned status: {}", response.status());
|
||||
let error_event = ChatErrorEvent {
|
||||
session_id: session_id.clone(),
|
||||
error: error_msg.clone(),
|
||||
};
|
||||
let _ = window.emit("chat-error", &error_event);
|
||||
return Err(error_msg);
|
||||
}
|
||||
|
||||
// 5. Processar stream e emitir tokens
|
||||
// IMPORTANTE: O Ollama envia tokens INCREMENTAIS (cada chunk contém apenas o novo conteúdo)
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut buffer = String::new();
|
||||
let mut full_content = String::new();
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| format!("Stream error: {}", e))?;
|
||||
let chunk_str = String::from_utf8_lossy(&chunk);
|
||||
buffer.push_str(&chunk_str);
|
||||
|
||||
// Processar linhas completas (separadas por \n)
|
||||
while let Some(pos) = buffer.find('\n') {
|
||||
let line = buffer[..pos].trim().to_string();
|
||||
buffer = buffer[pos + 1..].to_string();
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tentar deserializar como JSON do Ollama
|
||||
match serde_json::from_str::<serde_json::Value>(&line) {
|
||||
Ok(json) => {
|
||||
// Verificar se stream terminou primeiro
|
||||
let is_done = json.get("done").and_then(|d| d.as_bool()) == Some(true);
|
||||
|
||||
// Extrair conteúdo do chunk (Ollama envia tokens incrementais)
|
||||
if let Some(message) = json.get("message") {
|
||||
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
|
||||
// O Ollama envia apenas o NOVO conteúdo em cada chunk, não o acumulado
|
||||
// Então podemos emitir diretamente
|
||||
if !content.is_empty() {
|
||||
full_content.push_str(content);
|
||||
|
||||
// Emitir token para frontend
|
||||
let token_event = ChatTokenEvent {
|
||||
session_id: session_id.clone(),
|
||||
content: content.to_string(),
|
||||
done: false,
|
||||
};
|
||||
|
||||
if let Err(e) = window.emit("chat-token", &token_event) {
|
||||
log::warn!("Erro ao emitir token: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se stream terminou
|
||||
if is_done {
|
||||
// Emitir evento final
|
||||
let final_event = ChatTokenEvent {
|
||||
session_id: session_id.clone(),
|
||||
content: String::new(),
|
||||
done: true,
|
||||
};
|
||||
let _ = window.emit("chat-token", &final_event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Failed to parse JSON chunk: {} - Line: {}", e, line);
|
||||
// Continuar mesmo com erro de parse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Persistir sessão e mensagens no SQLite
|
||||
match Database::new(&app_handle) {
|
||||
Ok(db) => {
|
||||
let now = Utc::now();
|
||||
|
||||
// Criar ou atualizar sessão
|
||||
let session = if is_new_session && !title.is_empty() {
|
||||
ChatSession {
|
||||
id: session_id.clone(),
|
||||
title,
|
||||
emoji,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
} else {
|
||||
// Buscar sessão existente ou criar nova
|
||||
match db.get_session(&session_id) {
|
||||
Ok(Some(mut existing)) => {
|
||||
existing.updated_at = now;
|
||||
existing
|
||||
}
|
||||
_ => ChatSession {
|
||||
id: session_id.clone(),
|
||||
title: "Nova Conversa".to_string(),
|
||||
emoji: "💬".to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = db.create_session(&session) {
|
||||
log::warn!("Erro ao salvar sessão: {}", e);
|
||||
}
|
||||
|
||||
// Salvar mensagens do usuário
|
||||
for msg in &messages {
|
||||
let chat_msg = ChatMessage {
|
||||
id: None,
|
||||
session_id: session_id.clone(),
|
||||
role: msg.role.clone(),
|
||||
content: msg.content.clone(),
|
||||
metadata: msg.metadata.as_ref().and_then(|m| serde_json::to_string(m).ok()),
|
||||
created_at: now,
|
||||
};
|
||||
|
||||
if let Err(e) = db.add_message(&chat_msg) {
|
||||
log::warn!("Erro ao salvar mensagem: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Salvar mensagem final do assistente
|
||||
if !full_content.is_empty() {
|
||||
let assistant_msg = ChatMessage {
|
||||
id: None,
|
||||
session_id: session_id.clone(),
|
||||
role: "assistant".to_string(),
|
||||
content: full_content,
|
||||
metadata: None,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
|
||||
if let Err(e) = db.add_message(&assistant_msg) {
|
||||
log::warn!("Erro ao salvar mensagem do assistente: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Erro ao inicializar banco de dados: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(session_id)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -2678,6 +2966,7 @@ pub fn run() {
|
||||
.manage(Arc::new(Mutex::new(None::<Arc<Browser>>)) as BrowserState)
|
||||
.manage(Arc::new(Mutex::new(HashMap::<String, Arc<Mutex<()>>>::new())) as FileLockMap)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
chat_stream,
|
||||
check_ollama_installed,
|
||||
check_ollama_running,
|
||||
get_system_specs,
|
||||
|
||||
@@ -29,8 +29,8 @@ struct OllamaMessageResponse {
|
||||
|
||||
/// Cliente Ollama headless (para execução em background)
|
||||
pub struct OllamaClient {
|
||||
base_url: String,
|
||||
client: reqwest::Client,
|
||||
pub(crate) base_url: String,
|
||||
pub(crate) client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl OllamaClient {
|
||||
@@ -148,5 +148,109 @@ impl OllamaClient {
|
||||
|
||||
Ok(full_response.trim().to_string())
|
||||
}
|
||||
|
||||
/// Gera um título curto (3-5 palavras) para a pergunta do usuário
|
||||
pub async fn generate_title(&self, model: &str, user_input: &str) -> Result<String, String> {
|
||||
let system_prompt = "Você é um gerador de títulos. Responda APENAS com um título de 3-5 palavras que resuma a pergunta. Nada mais, sem explicações.";
|
||||
|
||||
let messages = vec![
|
||||
OllamaMessage {
|
||||
role: "system".to_string(),
|
||||
content: system_prompt.to_string(),
|
||||
},
|
||||
OllamaMessage {
|
||||
role: "user".to_string(),
|
||||
content: format!("Gere um título para: {}", user_input),
|
||||
},
|
||||
];
|
||||
|
||||
let request = OllamaChatRequest {
|
||||
model: model.to_string(),
|
||||
messages,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
let url = format!("{}/api/chat", self.base_url);
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.json(&request)
|
||||
.timeout(std::time::Duration::from_secs(10)) // Timeout curto para resposta rápida
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send title request: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Ollama returned status: {}", response.status()));
|
||||
}
|
||||
|
||||
// Ler stream e acumular resposta (limitado a ~50 caracteres)
|
||||
let mut full_response = String::new();
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
use futures_util::StreamExt;
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(|e| format!("Failed to read chunk: {}", e))?;
|
||||
let text = String::from_utf8_lossy(&chunk);
|
||||
|
||||
for line in text.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OllamaChunk>(line) {
|
||||
Ok(chunk_data) => {
|
||||
if let Some(message) = chunk_data.message {
|
||||
full_response.push_str(&message.content);
|
||||
|
||||
// Limitar tamanho para evitar respostas longas
|
||||
if full_response.len() > 50 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if chunk_data.done == Some(true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
// Se já temos resposta suficiente, parar
|
||||
if full_response.len() > 30 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let title = full_response.trim().to_string();
|
||||
|
||||
// Fallback se título estiver vazio ou muito longo
|
||||
if title.is_empty() || title.len() > 50 {
|
||||
// Extrair primeiras palavras da pergunta como fallback
|
||||
let words: Vec<&str> = user_input.split_whitespace().take(5).collect();
|
||||
Ok(words.join(" "))
|
||||
} else {
|
||||
Ok(title)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gera emoji baseado no título
|
||||
pub fn generate_emoji(title: &str) -> String {
|
||||
let title_lower = title.to_lowercase();
|
||||
|
||||
// Keywords para emojis
|
||||
if title_lower.contains("código") || title_lower.contains("program") || title_lower.contains("code") {
|
||||
"💻".to_string()
|
||||
} else if title_lower.contains("pergunta") || title_lower.contains("question") || title_lower.contains("como") {
|
||||
"❓".to_string()
|
||||
} else if title_lower.contains("explica") || title_lower.contains("explain") {
|
||||
"📚".to_string()
|
||||
} else if title_lower.contains("ajuda") || title_lower.contains("help") {
|
||||
"🆘".to_string()
|
||||
} else {
|
||||
"💬".to_string() // Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user