use std::time::Duration;
use tokio::sync::mpsc;
use tracing::{debug, error, info, warn};
use crate::llm::OllamaClient;
use crate::state::app_state::DueFeedback;
use crate::state::AppState;
pub const FEEDBACK_DEBOUNCE: Duration = Duration::from_secs(30);
pub async fn execute_due_feedback(
state: &AppState,
llm_client: &OllamaClient,
status_tx: &mpsc::UnboundedSender<String>,
) {
let due = state.take_due_feedback(FEEDBACK_DEBOUNCE).await;
if due.is_empty() {
return;
}
for batch in due {
let state = state.clone();
let llm_client = llm_client.clone();
let status_tx = status_tx.clone();
tokio::spawn(async move {
process_feedback_batch(state, llm_client, status_tx, batch).await;
});
}
}
fn describe(batch: &DueFeedback) -> String {
match (batch.server_id, batch.client_id) {
(Some(sid), _) => format!("server #{}", sid.as_u32()),
(_, Some(cid)) => format!("client #{}", cid.as_u32()),
_ => "unknown instance".to_string(),
}
}
async fn process_feedback_batch(
state: AppState,
llm_client: OllamaClient,
status_tx: mpsc::UnboundedSender<String>,
batch: DueFeedback,
) {
let who = describe(&batch);
info!(
"Processing {} feedback entries for {}",
batch.entries.len(),
who
);
let _ = status_tx.send(format!(
"[INFO] Processing {} feedback entries for {}",
batch.entries.len(),
who
));
let actions = match crate::llm::action_helper::call_llm_for_feedback(
&llm_client,
&state,
batch.server_id,
batch.client_id,
&batch.instructions,
&batch.instruction,
&batch.memory,
&batch.entries,
&status_tx,
)
.await
{
Ok(actions) => actions,
Err(e) => {
error!(
"Feedback processing failed for {} ({} entries dropped): {}",
who,
batch.entries.len(),
e
);
let _ = status_tx.send(format!(
"[ERROR] Feedback processing failed for {} ({} entries dropped): {}",
who,
batch.entries.len(),
e
));
return;
}
};
if actions.is_empty() {
debug!("Feedback for {} produced no adjustments", who);
return;
}
apply_feedback_actions(&state, &llm_client, &status_tx, &batch, actions).await;
}
enum Route {
Instruction(String),
Handler(crate::llm::CommonAction),
Passthrough(serde_json::Value),
}
fn route(action: serde_json::Value) -> Route {
use crate::llm::CommonAction;
match CommonAction::from_json(&action) {
Ok(CommonAction::UpdateInstruction { instruction }) => Route::Instruction(instruction),
Ok(CommonAction::UpdateClientInstruction { instruction, .. }) => {
Route::Instruction(instruction)
}
Ok(
common @ (CommonAction::OpenServer { .. }
| CommonAction::CloseServer { .. }
| CommonAction::CloseAllServers
| CommonAction::ChangeModel { .. }
| CommonAction::ScheduleTask { .. }
| CommonAction::CancelTask { .. }
| CommonAction::OpenClient { .. }
| CommonAction::CloseClient { .. }
| CommonAction::CloseAllClients
| CommonAction::CloseConnectionById { .. }
| CommonAction::ReconnectClient { .. }
| CommonAction::ShowMessage { .. }),
) => Route::Handler(common),
_ => Route::Passthrough(action),
}
}
async fn apply_feedback_actions(
state: &AppState,
llm_client: &OllamaClient,
status_tx: &mpsc::UnboundedSender<String>,
batch: &DueFeedback,
actions: Vec<serde_json::Value>,
) {
let who = describe(batch);
let mut management = Vec::new();
let mut passthrough = Vec::new();
for action in actions {
match route(action) {
Route::Instruction(instruction) => {
if let Some(sid) = batch.server_id {
state.set_instruction(sid, instruction.clone()).await;
} else if let Some(cid) = batch.client_id {
state
.set_instruction_for_client(cid, instruction.clone())
.await;
}
info!("Feedback updated the instruction for {}", who);
let _ = status_tx.send(format!(
"[INFO] Feedback updated the instruction for {}: {}",
who,
crate::utils::truncate_for_log(&instruction, 120)
));
}
Route::Handler(common) => management.push(common),
Route::Passthrough(value) => passthrough.push(value),
}
}
if !management.is_empty() {
let mut handler = crate::events::EventHandler::new(state.clone(), llm_client.clone());
for action in management {
if let Err(e) = handler
.execute_server_management_action(action, status_tx)
.await
{
error!("Feedback adjustment failed for {}: {}", who, e);
let _ = status_tx.send(format!(
"[ERROR] Feedback adjustment failed for {}: {}",
who, e
));
}
}
}
if passthrough.is_empty() {
return;
}
match crate::llm::execute_actions(passthrough, state, None, batch.server_id, batch.client_id)
.await
{
Ok(result) => {
let failures = result.failure_summary();
for msg in result.messages {
let _ = status_tx.send(msg);
}
if let Some(summary) = failures {
warn!("Feedback adjustment for {} partly failed: {}", who, summary);
let _ = status_tx.send(format!(
"[WARN] Feedback adjustment for {} partly failed: {}",
who, summary
));
}
}
Err(e) => {
error!("Failed to apply feedback adjustments for {}: {}", who, e);
let _ = status_tx.send(format!(
"[ERROR] Failed to apply feedback adjustments for {}: {}",
who, e
));
}
}
}