mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
community fixes
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/RightNow-AI/openfang"
|
||||
|
||||
@@ -7007,8 +7007,8 @@ pub async fn set_provider_key(
|
||||
"\n[default_model]\nprovider = \"{}\"\nmodel = \"{}\"\napi_key_env = \"{}\"\n",
|
||||
name, model_id, env_var
|
||||
);
|
||||
backup_config(&config_path);
|
||||
if let Ok(existing) = std::fs::read_to_string(&config_path) {
|
||||
// Remove existing [default_model] section if present, then append
|
||||
let cleaned = remove_toml_section(&existing, "default_model");
|
||||
let _ = std::fs::write(&config_path, format!("{}\n{}", cleaned.trim(), update_toml));
|
||||
} else {
|
||||
@@ -10839,6 +10839,12 @@ pub async fn auth_check(
|
||||
}
|
||||
|
||||
/// Remove a `[section]` and its contents from a TOML string.
|
||||
#[allow(dead_code)]
|
||||
fn backup_config(config_path: &std::path::Path) {
|
||||
let backup = config_path.with_extension("toml.bak");
|
||||
let _ = std::fs::copy(config_path, backup);
|
||||
}
|
||||
|
||||
fn remove_toml_section(content: &str, section: &str) -> String {
|
||||
let header = format!("[{}]", section);
|
||||
let mut result = String::new();
|
||||
|
||||
@@ -393,7 +393,7 @@ function agentsPage() {
|
||||
},
|
||||
|
||||
// ── Multi-step wizard navigation ──
|
||||
openSpawnWizard() {
|
||||
async openSpawnWizard() {
|
||||
this.showSpawnModal = true;
|
||||
this.spawnStep = 1;
|
||||
this.spawnMode = 'wizard';
|
||||
@@ -401,8 +401,18 @@ function agentsPage() {
|
||||
this.selectedPreset = '';
|
||||
this.soulContent = '';
|
||||
this.spawnForm.name = '';
|
||||
this.spawnForm.provider = 'groq';
|
||||
this.spawnForm.model = 'llama-3.3-70b-versatile';
|
||||
this.spawnForm.systemPrompt = 'You are a helpful assistant.';
|
||||
this.spawnForm.profile = 'full';
|
||||
try {
|
||||
var res = await fetch('/api/status');
|
||||
if (res.ok) {
|
||||
var status = await res.json();
|
||||
if (status.default_provider) this.spawnForm.provider = status.default_provider;
|
||||
if (status.default_model) this.spawnForm.model = status.default_model;
|
||||
}
|
||||
} catch(e) { /* keep hardcoded defaults */ }
|
||||
},
|
||||
|
||||
nextStep() {
|
||||
|
||||
@@ -780,7 +780,7 @@ async fn dispatch_message(
|
||||
send_lifecycle_reaction(adapter, &message.sender, msg_id, AgentPhase::Error).await;
|
||||
}
|
||||
warn!("Agent error for {agent_id}: {e}");
|
||||
let err_msg = format!("Agent error: {e}");
|
||||
let err_msg = sanitize_agent_error(&e.to_string());
|
||||
send_response(
|
||||
adapter,
|
||||
&message.sender,
|
||||
@@ -803,6 +803,76 @@ async fn dispatch_message(
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_agent_error(raw: &str) -> String {
|
||||
let lower = raw.to_lowercase();
|
||||
|
||||
if lower.contains("rate limit")
|
||||
|| lower.contains("rate_limit")
|
||||
|| lower.contains("429")
|
||||
|| lower.contains("too many requests")
|
||||
|| lower.contains("resource_exhausted")
|
||||
{
|
||||
return "Rate limit reached, please try again later.".to_string();
|
||||
}
|
||||
|
||||
if lower.contains("authentication")
|
||||
|| lower.contains("unauthorized")
|
||||
|| lower.contains("invalid api key")
|
||||
|| lower.contains("invalid x-goog-api-key")
|
||||
|| lower.contains("incorrect api key")
|
||||
|| lower.contains("permission denied")
|
||||
|| lower.contains("billing")
|
||||
|| lower.contains("quota exceeded")
|
||||
{
|
||||
return "Service temporarily unavailable.".to_string();
|
||||
}
|
||||
|
||||
if lower.contains("context length")
|
||||
|| lower.contains("token limit")
|
||||
|| lower.contains("too many tokens")
|
||||
|| lower.contains("maximum context")
|
||||
|| lower.contains("max_tokens")
|
||||
|| lower.contains("context window")
|
||||
{
|
||||
return "Message too long, try a shorter request.".to_string();
|
||||
}
|
||||
|
||||
if lower.contains("overloaded")
|
||||
|| lower.contains("503")
|
||||
|| lower.contains("502")
|
||||
|| lower.contains("server error")
|
||||
|| lower.contains("internal error")
|
||||
{
|
||||
return "The AI service is temporarily overloaded, please try again shortly.".to_string();
|
||||
}
|
||||
|
||||
if lower.contains("timeout") || lower.contains("timed out") || lower.contains("deadline") {
|
||||
return "Request timed out, please try again.".to_string();
|
||||
}
|
||||
|
||||
if lower.contains("model not found") || lower.contains("model_not_found") {
|
||||
return "The requested model is currently unavailable.".to_string();
|
||||
}
|
||||
|
||||
let cleaned = raw
|
||||
.strip_prefix("LLM driver error: ")
|
||||
.or_else(|| raw.strip_prefix("Agent error: "))
|
||||
.unwrap_or(raw);
|
||||
|
||||
if let Some(first_sentence_end) = cleaned.find(". ") {
|
||||
let first = &cleaned[..=first_sentence_end];
|
||||
if first.len() < cleaned.len() / 2 {
|
||||
return format!("Agent error: {first}");
|
||||
}
|
||||
}
|
||||
|
||||
if cleaned.contains('{') || cleaned.len() > 200 {
|
||||
return "Something went wrong processing your request. Please try again.".to_string();
|
||||
}
|
||||
|
||||
format!("Agent error: {cleaned}")
|
||||
}
|
||||
|
||||
/// Detect image format from the first few magic bytes.
|
||||
///
|
||||
/// Returns `Some("image/...")` for JPEG, PNG, GIF, and WebP.
|
||||
@@ -1012,7 +1082,7 @@ async fn dispatch_with_blocks(
|
||||
send_lifecycle_reaction(adapter, &message.sender, msg_id, AgentPhase::Error).await;
|
||||
}
|
||||
warn!("Agent error for {agent_id}: {e}");
|
||||
let err_msg = format!("Agent error: {e}");
|
||||
let err_msg = sanitize_agent_error(&e.to_string());
|
||||
send_response(
|
||||
adapter,
|
||||
&message.sender,
|
||||
|
||||
@@ -794,11 +794,36 @@ enum SystemCommands {
|
||||
},
|
||||
}
|
||||
|
||||
fn config_log_level() -> String {
|
||||
let config_path = if let Ok(home) = std::env::var("OPENFANG_HOME") {
|
||||
std::path::PathBuf::from(home).join("config.toml")
|
||||
} else {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(".openfang")
|
||||
.join("config.toml")
|
||||
};
|
||||
if let Ok(content) = std::fs::read_to_string(config_path) {
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("log_level") {
|
||||
if let Some(val) = trimmed.split('=').nth(1) {
|
||||
let level = val.trim().trim_matches('"').trim_matches('\'');
|
||||
if !level.is_empty() {
|
||||
return level.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
fn init_tracing_stderr() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(config_log_level())),
|
||||
)
|
||||
.init();
|
||||
}
|
||||
@@ -824,7 +849,7 @@ fn init_tracing_file() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(config_log_level())),
|
||||
)
|
||||
.with_writer(std::sync::Mutex::new(file))
|
||||
.with_ansi(false)
|
||||
@@ -4691,6 +4716,8 @@ fn cmd_config_set(key: &str, value: &str) {
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let _ = std::fs::copy(&config_path, config_path.with_extension("toml.bak"));
|
||||
|
||||
std::fs::write(&config_path, &serialized).unwrap_or_else(|e| {
|
||||
ui::error(&format!("Failed to write config: {e}"));
|
||||
std::process::exit(1);
|
||||
@@ -4757,6 +4784,8 @@ fn cmd_config_unset(key: &str) {
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let _ = std::fs::copy(&config_path, config_path.with_extension("toml.bak"));
|
||||
|
||||
std::fs::write(&config_path, &serialized).unwrap_or_else(|e| {
|
||||
ui::error(&format!("Failed to write config: {e}"));
|
||||
std::process::exit(1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Compile-time embedded Hand definitions.
|
||||
|
||||
use crate::{HandDefinition, HandError};
|
||||
use crate::{parse_hand_toml, HandDefinition, HandError};
|
||||
|
||||
/// Returns all bundled hand definitions as (id, HAND.toml content, SKILL.md content).
|
||||
pub fn bundled_hands() -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
@@ -55,7 +55,7 @@ pub fn parse_bundled(
|
||||
skill_content: &str,
|
||||
) -> Result<HandDefinition, HandError> {
|
||||
let mut def: HandDefinition =
|
||||
toml::from_str(toml_content).map_err(|e| HandError::TomlParse(e.to_string()))?;
|
||||
parse_hand_toml(toml_content).map_err(|e| HandError::TomlParse(e.to_string()))?;
|
||||
if !skill_content.is_empty() {
|
||||
def.skill_content = Some(skill_content.to_string());
|
||||
}
|
||||
|
||||
@@ -306,6 +306,20 @@ fn default_temperature() -> f32 {
|
||||
0.7
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct HandTomlWrapper {
|
||||
hand: HandDefinition,
|
||||
}
|
||||
|
||||
/// Parse HAND.toml content, supporting both flat format and `[hand]` table format.
|
||||
pub fn parse_hand_toml(content: &str) -> Result<HandDefinition, toml::de::Error> {
|
||||
if let Ok(def) = toml::from_str::<HandDefinition>(content) {
|
||||
return Ok(def);
|
||||
}
|
||||
let wrapper: HandTomlWrapper = toml::from_str(content)?;
|
||||
Ok(wrapper.hand)
|
||||
}
|
||||
|
||||
/// Complete Hand definition — parsed from HAND.toml.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HandDefinition {
|
||||
@@ -798,4 +812,50 @@ metrics = []
|
||||
assert!(install.macos.is_none());
|
||||
assert!(install.windows.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hand_toml_flat_format() {
|
||||
let toml_str = r#"
|
||||
id = "test"
|
||||
name = "Test Hand"
|
||||
description = "A test hand"
|
||||
category = "content"
|
||||
tools = ["shell_exec"]
|
||||
|
||||
[agent]
|
||||
name = "test-hand"
|
||||
description = "Test agent"
|
||||
system_prompt = "You are a test agent."
|
||||
|
||||
[dashboard]
|
||||
metrics = []
|
||||
"#;
|
||||
let def = parse_hand_toml(toml_str).unwrap();
|
||||
assert_eq!(def.id, "test");
|
||||
assert_eq!(def.name, "Test Hand");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hand_toml_wrapped_format() {
|
||||
let toml_str = r#"
|
||||
[hand]
|
||||
id = "test"
|
||||
name = "Test Hand"
|
||||
description = "A test hand"
|
||||
category = "content"
|
||||
tools = ["shell_exec"]
|
||||
|
||||
[hand.agent]
|
||||
name = "test-hand"
|
||||
description = "Test agent"
|
||||
system_prompt = "You are a test agent."
|
||||
|
||||
[hand.dashboard]
|
||||
metrics = []
|
||||
"#;
|
||||
let def = parse_hand_toml(toml_str).unwrap();
|
||||
assert_eq!(def.id, "test");
|
||||
assert_eq!(def.name, "Test Hand");
|
||||
assert_eq!(def.agent.name, "test-hand");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1602,7 +1602,7 @@ impl OpenFangKernel {
|
||||
label: None,
|
||||
});
|
||||
|
||||
// Check if auto-compaction is needed: message-count OR token-count trigger
|
||||
// Check if auto-compaction is needed: message-count OR token-count OR quota-headroom trigger
|
||||
let needs_compact = {
|
||||
use openfang_runtime::compactor::{
|
||||
estimate_token_count, needs_compaction as check_compact,
|
||||
@@ -1624,7 +1624,23 @@ impl OpenFangKernel {
|
||||
"Token-based compaction triggered (messages below threshold but tokens above)"
|
||||
);
|
||||
}
|
||||
by_messages || by_tokens
|
||||
let by_quota = if let Some(headroom) = self.scheduler.token_headroom(agent_id) {
|
||||
let threshold = (headroom as f64 * 0.8) as u64;
|
||||
if estimated as u64 > threshold && session.messages.len() > 4 {
|
||||
info!(
|
||||
agent_id = %agent_id,
|
||||
estimated_tokens = estimated,
|
||||
quota_headroom = headroom,
|
||||
"Quota-headroom compaction triggered (session would consume >80% of remaining quota)"
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
by_messages || by_tokens || by_quota
|
||||
};
|
||||
|
||||
let tools = self.available_tools(agent_id);
|
||||
@@ -2094,6 +2110,42 @@ impl OpenFangKernel {
|
||||
label: None,
|
||||
});
|
||||
|
||||
// Pre-emptive compaction: compact before LLM call if session is large or quota headroom is low
|
||||
{
|
||||
use openfang_runtime::compactor::{
|
||||
estimate_token_count, needs_compaction as check_compact,
|
||||
needs_compaction_by_tokens, CompactionConfig,
|
||||
};
|
||||
let config = CompactionConfig::default();
|
||||
let by_messages = check_compact(&session, &config);
|
||||
let estimated = estimate_token_count(
|
||||
&session.messages,
|
||||
Some(&entry.manifest.model.system_prompt),
|
||||
None,
|
||||
);
|
||||
let by_tokens = needs_compaction_by_tokens(estimated, &config);
|
||||
let by_quota = if let Some(headroom) = self.scheduler.token_headroom(agent_id) {
|
||||
let threshold = (headroom as f64 * 0.8) as u64;
|
||||
estimated as u64 > threshold && session.messages.len() > 4
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if by_messages || by_tokens || by_quota {
|
||||
info!(agent_id = %agent_id, messages = session.messages.len(), estimated_tokens = estimated, "Pre-emptive compaction before LLM call");
|
||||
match self.compact_agent_session(agent_id).await {
|
||||
Ok(msg) => {
|
||||
info!(agent_id = %agent_id, "{msg}");
|
||||
if let Ok(Some(reloaded)) = self.memory.get_session(session.id) {
|
||||
session = reloaded;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(agent_id = %agent_id, "Pre-emptive compaction failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let messages_before = session.messages.len();
|
||||
|
||||
let tools = self.available_tools(agent_id);
|
||||
|
||||
@@ -130,6 +130,19 @@ impl AgentScheduler {
|
||||
.get(&agent_id)
|
||||
.map(|t| (t.total_tokens, t.tool_calls))
|
||||
}
|
||||
|
||||
/// Returns remaining token headroom before quota is hit.
|
||||
/// Returns `None` if no token quota is configured (unlimited).
|
||||
pub fn token_headroom(&self, agent_id: AgentId) -> Option<u64> {
|
||||
let quota = self.quotas.get(&agent_id)?;
|
||||
if quota.max_llm_tokens_per_hour == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut tracker = self.usage.get_mut(&agent_id)?;
|
||||
tracker.reset_if_expired();
|
||||
let used = tracker.total_tokens;
|
||||
Some(quota.max_llm_tokens_per_hour.saturating_sub(used))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AgentScheduler {
|
||||
|
||||
@@ -275,12 +275,18 @@ pub fn build_canonical_context_message(ctx: &PromptContext) -> Option<String> {
|
||||
///
|
||||
/// Also used by `agent_loop.rs` to append recalled memories after DB lookup.
|
||||
pub fn build_memory_section(memories: &[(String, String)]) -> String {
|
||||
let mut out = String::from(
|
||||
"## Memory\n\
|
||||
- When the user asks about something from a previous conversation, use memory_recall first.\n\
|
||||
- Store important preferences, decisions, and context with memory_store for future use.",
|
||||
);
|
||||
if !memories.is_empty() {
|
||||
let mut out = String::from("## Memory\n");
|
||||
if memories.is_empty() {
|
||||
out.push_str(
|
||||
"- When the user asks about something from a previous conversation, use memory_recall first.\n\
|
||||
- Store important preferences, decisions, and context with memory_store for future use.",
|
||||
);
|
||||
} else {
|
||||
out.push_str(
|
||||
"- Use the recalled memories below to inform your responses.\n\
|
||||
- Only call memory_recall if you need information not already shown here.\n\
|
||||
- Store important preferences, decisions, and context with memory_store for future use.",
|
||||
);
|
||||
out.push_str("\n\nRecalled memories:\n");
|
||||
for (key, content) in memories.iter().take(5) {
|
||||
let capped = cap_str(content, 500);
|
||||
@@ -728,7 +734,7 @@ mod tests {
|
||||
fn test_memory_section_empty() {
|
||||
let section = build_memory_section(&[]);
|
||||
assert!(section.contains("## Memory"));
|
||||
assert!(section.contains("memory_recall"));
|
||||
assert!(section.contains("use memory_recall first"));
|
||||
assert!(!section.contains("Recalled memories"));
|
||||
}
|
||||
|
||||
@@ -742,6 +748,8 @@ mod tests {
|
||||
assert!(section.contains("Recalled memories"));
|
||||
assert!(section.contains("[pref] User likes dark mode"));
|
||||
assert!(section.contains("[ctx] Working on Rust project"));
|
||||
assert!(section.contains("Use the recalled memories below"));
|
||||
assert!(!section.contains("use memory_recall first"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -72,7 +72,7 @@ pub enum OpenFangError {
|
||||
Serialization(String),
|
||||
|
||||
/// The agent loop exceeded the maximum iteration count.
|
||||
#[error("Max iterations exceeded: {0}")]
|
||||
#[error("Max iterations exceeded ({0}). Configure a higher limit in agent.toml under [autonomous] max_iterations")]
|
||||
MaxIterationsExceeded(u32),
|
||||
|
||||
/// The kernel is shutting down.
|
||||
|
||||
Reference in New Issue
Block a user