fix: Drop stream_tx after LLM spawn + ADD_SUGGESTION single-arg + lowercase fix + sync_bas_to_work
- drop(stream_tx) after spawning LLM task so stream_rx.recv() loop ends when LLM finishes. Without this, the streaming loop hung forever and is_complete:true + suggestions were never sent to WebSocket clients. - Add single-arg ADD_SUGGESTION "text" syntax (registered LAST for highest Rhai priority so it matches before 2-arg form). - convert_keywords_to_lowercase() now only lowercases Rhai built-in keywords (IF, ELSE, WHILE, etc.), not custom syntax keywords (TALK, HEAR, ADD_SUGGESTION) which are case-sensitive in Rhai. - sync_bas_to_work() downloads changed .bas files from S3 to work dir when etag changes, preventing stale local copies used by compiler.
This commit is contained in:
parent
1ae0ad7051
commit
e6cd0ff02b
4 changed files with 112 additions and 39 deletions
|
|
@ -69,13 +69,15 @@ pub fn add_suggestion_keyword(
|
||||||
) {
|
) {
|
||||||
// Each closure needs its own Arc<redis::Client> and UserSession clone
|
// Each closure needs its own Arc<redis::Client> and UserSession clone
|
||||||
let cache = state.cache.clone();
|
let cache = state.cache.clone();
|
||||||
|
let cache2 = state.cache.clone();
|
||||||
let cache3 = state.cache.clone();
|
let cache3 = state.cache.clone();
|
||||||
let cache4 = state.cache.clone();
|
let cache4 = state.cache.clone();
|
||||||
|
let user_session = user_session.clone();
|
||||||
|
let user_session2 = user_session.clone();
|
||||||
let user_session3 = user_session.clone();
|
let user_session3 = user_session.clone();
|
||||||
let user_session4 = user_session.clone();
|
let user_session4 = user_session.clone();
|
||||||
|
|
||||||
// ADD_SUGGESTION_TOOL "tool_name" as "button text"
|
// ADD_SUGGESTION_TOOL "tool_name" as "button text"
|
||||||
// Note: compiler converts AS -> as (lowercase keywords), so we use lowercase here
|
|
||||||
engine
|
engine
|
||||||
.register_custom_syntax(
|
.register_custom_syntax(
|
||||||
["ADD_SUGGESTION_TOOL", "$expr$", "as", "$expr$"],
|
["ADD_SUGGESTION_TOOL", "$expr$", "as", "$expr$"],
|
||||||
|
|
@ -106,14 +108,14 @@ pub fn add_suggestion_keyword(
|
||||||
let text_value = context.eval_expression_tree(&inputs[0])?.to_string();
|
let text_value = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||||
let button_text = context.eval_expression_tree(&inputs[1])?.to_string();
|
let button_text = context.eval_expression_tree(&inputs[1])?.to_string();
|
||||||
|
|
||||||
add_text_suggestion(cache3.as_ref(), &user_session3, &text_value, &button_text)?;
|
add_text_suggestion(cache2.as_ref(), &user_session2, &text_value, &button_text)?;
|
||||||
|
|
||||||
Ok(Dynamic::UNIT)
|
Ok(Dynamic::UNIT)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("valid syntax registration");
|
.expect("valid syntax registration");
|
||||||
|
|
||||||
// ADD_SUGGESTION "context_name" as "button text"
|
// ADD_SUGGESTION "context_name" as "button text" (register BEFORE simple form so simple form has higher priority)
|
||||||
engine
|
engine
|
||||||
.register_custom_syntax(
|
.register_custom_syntax(
|
||||||
["ADD_SUGGESTION", "$expr$", "as", "$expr$"],
|
["ADD_SUGGESTION", "$expr$", "as", "$expr$"],
|
||||||
|
|
@ -123,9 +125,30 @@ pub fn add_suggestion_keyword(
|
||||||
let button_text = context.eval_expression_tree(&inputs[1])?.to_string();
|
let button_text = context.eval_expression_tree(&inputs[1])?.to_string();
|
||||||
|
|
||||||
add_context_suggestion(
|
add_context_suggestion(
|
||||||
|
cache3.as_ref(),
|
||||||
|
&user_session3,
|
||||||
|
&context_name,
|
||||||
|
&button_text,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Dynamic::UNIT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("valid syntax registration");
|
||||||
|
|
||||||
|
// ADD_SUGGESTION "button text" (simple form - sends message on click)
|
||||||
|
// Registered LAST so it has HIGHEST priority — Rhai tries this first, falls back to 2-arg form
|
||||||
|
engine
|
||||||
|
.register_custom_syntax(
|
||||||
|
["ADD_SUGGESTION", "$expr$"],
|
||||||
|
true,
|
||||||
|
move |context, inputs| {
|
||||||
|
let button_text = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||||
|
|
||||||
|
add_text_suggestion(
|
||||||
cache4.as_ref(),
|
cache4.as_ref(),
|
||||||
&user_session4,
|
&user_session4,
|
||||||
&context_name,
|
&button_text,
|
||||||
&button_text,
|
&button_text,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1260,17 +1260,19 @@ impl ScriptService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert BASIC keywords to lowercase without touching variables
|
/// Convert BASIC keywords to lowercase without touching variables
|
||||||
/// Uses the centralized keyword list from get_all_keywords()
|
/// Only lowercases Rhai built-in keywords (if, while, for, etc.)
|
||||||
pub fn convert_keywords_to_lowercase(script: &str) -> String {
|
/// Custom syntax keywords (TALK, HEAR, ADD_SUGGESTION, etc.) must remain uppercase
|
||||||
use crate::basic::keywords::get_all_keywords;
|
pub fn convert_keywords_to_lowercase(script: &str) -> String {
|
||||||
|
let rhai_builtins = [
|
||||||
let keywords = get_all_keywords();
|
"IF", "ELSE", "WHILE", "FOR", "IN", "LOOP", "RETURN", "LET",
|
||||||
|
"CONST", "IMPORT", "EXPORT", "FN", "PRIVATE", "SWITCH", "MATCH",
|
||||||
|
"TRUE", "FALSE", "BREAK", "CONTINUE", "DO", "TRY", "CATCH", "THROW",
|
||||||
|
];
|
||||||
|
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
for line in script.lines() {
|
for line in script.lines() {
|
||||||
let mut processed_line = line.to_string();
|
let mut processed_line = line.to_string();
|
||||||
for keyword in &keywords {
|
for keyword in &rhai_builtins {
|
||||||
// Use word boundaries to avoid replacing parts of variable names
|
|
||||||
let pattern = format!(r"\b{}\b", regex::escape(keyword));
|
let pattern = format!(r"\b{}\b", regex::escape(keyword));
|
||||||
if let Ok(re) = regex::Regex::new(&pattern) {
|
if let Ok(re) = regex::Regex::new(&pattern) {
|
||||||
processed_line = re.replace_all(&processed_line, keyword.to_lowercase()).to_string();
|
processed_line = re.replace_all(&processed_line, keyword.to_lowercase()).to_string();
|
||||||
|
|
@ -1280,7 +1282,7 @@ impl ScriptService {
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Convert ALL multi-word keywords to underscore versions (function calls)
|
/// Convert ALL multi-word keywords to underscore versions (function calls)
|
||||||
|
|
|
||||||
|
|
@ -851,6 +851,10 @@ impl BotOrchestrator {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Drop the original stream_tx so stream_rx.recv() loop ends
|
||||||
|
// when the LLM task finishes and drops its clone.
|
||||||
|
drop(stream_tx);
|
||||||
|
|
||||||
// Wait for cancellation to abort LLM task
|
// Wait for cancellation to abort LLM task
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if cancel_rx_for_abort.recv().await.is_ok() {
|
if cancel_rx_for_abort.recv().await.is_ok() {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,10 @@ impl DriveMonitor {
|
||||||
Ok(_) => log::info!("Added/updated drive_files for: {} ({})", full_key, file_type),
|
Ok(_) => log::info!("Added/updated drive_files for: {} ({})", full_key, file_type),
|
||||||
Err(e) => log::error!("Failed to upsert {}: {}", full_key, e),
|
Err(e) => log::error!("Failed to upsert {}: {}", full_key, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if file_type == "bas" {
|
||||||
|
self.sync_bas_to_work(bot_name, &obj.key).await;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log::debug!("{} unchanged, skipping upsert", full_key);
|
log::debug!("{} unchanged, skipping upsert", full_key);
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +253,46 @@ impl DriveMonitor {
|
||||||
let _ = self.file_repo.mark_indexed(self.bot_id, &full_key);
|
let _ = self.file_repo.mark_indexed(self.bot_id, &full_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn sync_bas_to_work(&self, bot_name: &str, s3_key: &str) {
|
||||||
|
let s3 = match &self.state.drive {
|
||||||
|
Some(s3) => s3,
|
||||||
|
None => {
|
||||||
|
log::error!("S3 client not available for .bas sync");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = match s3.get_object_direct(&self.bucket_name, s3_key).await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to download .bas from {}/{}: {}", self.bucket_name, s3_key, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let work_dir = self.work_root.join(format!("{}.gbai/{}.gbdialog", bot_name, bot_name));
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&work_dir) {
|
||||||
|
log::error!("Failed to create work dir {}: {}", work_dir.display(), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = s3_key.split('/').next_back().unwrap_or(s3_key);
|
||||||
|
let work_path = work_dir.join(file_name);
|
||||||
|
|
||||||
|
match String::from_utf8(data) {
|
||||||
|
Ok(content) => {
|
||||||
|
if let Err(e) = std::fs::write(&work_path, &content) {
|
||||||
|
log::error!("Failed to write {} to work dir: {}", work_path.display(), e);
|
||||||
|
} else {
|
||||||
|
log::info!("Synced {} to work dir {}", s3_key, work_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to parse .bas as UTF-8: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "research", feature = "llm"))]
|
#[cfg(any(feature = "research", feature = "llm"))]
|
||||||
fn delete_kb_file_vectors(&self, bot_name: &str, _full_key: &str, s3_key: &str) {
|
fn delete_kb_file_vectors(&self, bot_name: &str, _full_key: &str, s3_key: &str) {
|
||||||
let parsed = match parse_kb_path(s3_key) {
|
let parsed = match parse_kb_path(s3_key) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue