feat: Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts
This commit is contained in:
@@ -671,6 +671,7 @@ bitflags::bitflags! {
|
||||
const SESSION = 1 << 2;
|
||||
const RAG = 1 << 3;
|
||||
const AGENT = 1 << 4;
|
||||
const FUNCTION_CALLING = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -371,6 +371,9 @@ impl RequestContext {
|
||||
if self.rag.is_some() {
|
||||
flags |= StateFlags::RAG;
|
||||
}
|
||||
if self.app.config.function_calling_support {
|
||||
flags |= StateFlags::FUNCTION_CALLING;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
@@ -450,6 +453,34 @@ impl RequestContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tools_info(&self) -> Result<String> {
|
||||
if !self.app.config.function_calling_support {
|
||||
bail!(
|
||||
"Function calling is disabled. Enable it by setting `function_calling_support: true` in your config or running `.set function_calling_support true`."
|
||||
);
|
||||
}
|
||||
let role = self.extract_role(&self.app.config)?;
|
||||
match self.select_functions(&role) {
|
||||
None => Ok("No tools enabled for the next request.\n".to_string()),
|
||||
Some(functions) => {
|
||||
let mut names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
|
||||
names.sort_unstable();
|
||||
let mut out = format!(
|
||||
"Tools enabled for the next request: {}\n\n",
|
||||
functions.len()
|
||||
);
|
||||
|
||||
for name in names {
|
||||
out.push_str(" ");
|
||||
out.push_str(name);
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_sessions(&self) -> Vec<String> {
|
||||
list_file_names(self.sessions_dir(), ".yaml")
|
||||
}
|
||||
@@ -4062,9 +4093,47 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_empty_context() {
|
||||
fn state_empty_context_has_no_context_flags() {
|
||||
let ctx = create_test_ctx();
|
||||
assert_eq!(ctx.state(), StateFlags::empty());
|
||||
|
||||
let state = ctx.state();
|
||||
|
||||
assert!(!state.contains(StateFlags::ROLE));
|
||||
assert!(!state.contains(StateFlags::SESSION));
|
||||
assert!(!state.contains(StateFlags::SESSION_EMPTY));
|
||||
assert!(!state.contains(StateFlags::AGENT));
|
||||
assert!(!state.contains(StateFlags::RAG));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_includes_function_calling_when_app_enables_it() {
|
||||
let ctx = create_test_ctx();
|
||||
|
||||
assert!(ctx.state().contains(StateFlags::FUNCTION_CALLING));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_omits_function_calling_when_app_disables_it() {
|
||||
let app_state = {
|
||||
let config = AppConfig {
|
||||
function_calling_support: false,
|
||||
..AppConfig::default()
|
||||
};
|
||||
Arc::new(AppState {
|
||||
config: Arc::new(config),
|
||||
vault: Arc::new(Vault::default()),
|
||||
mcp_factory: Arc::new(McpFactory::default()),
|
||||
rag_cache: Arc::new(RagCache::default()),
|
||||
mcp_config: None,
|
||||
mcp_log_path: None,
|
||||
mcp_registry: None,
|
||||
functions: Functions::default(),
|
||||
})
|
||||
};
|
||||
|
||||
let ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
|
||||
assert!(!ctx.state().contains(StateFlags::FUNCTION_CALLING));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4092,6 +4161,89 @@ mod tests {
|
||||
assert!(state.contains(StateFlags::SESSION_EMPTY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_info_returns_message_when_no_tools_enabled() {
|
||||
let ctx = create_test_ctx();
|
||||
|
||||
let info = ctx.tools_info().unwrap();
|
||||
|
||||
assert!(
|
||||
info.contains("No tools enabled"),
|
||||
"expected 'No tools enabled' message, got: {info}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_info_lists_enabled_tool_names_alphabetically() {
|
||||
let mut ctx = create_test_ctx();
|
||||
ctx.tool_scope.functions.append_todo_functions();
|
||||
let mut role = Role::new("r", "p");
|
||||
role.set_enabled_tools(Some(vec!["all".to_string()]));
|
||||
ctx.role = Some(role);
|
||||
|
||||
let info = ctx.tools_info().unwrap();
|
||||
|
||||
assert!(
|
||||
info.contains("Tools enabled for the next request:"),
|
||||
"expected count line, got: {info}"
|
||||
);
|
||||
assert!(
|
||||
info.contains("todo__init"),
|
||||
"expected todo__init in output, got: {info}"
|
||||
);
|
||||
|
||||
let positions: Vec<usize> = info
|
||||
.lines()
|
||||
.filter(|line| line.trim().starts_with("todo__"))
|
||||
.enumerate()
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
assert!(
|
||||
!positions.is_empty(),
|
||||
"expected at least one todo__ entry, got: {info}"
|
||||
);
|
||||
|
||||
let todo_lines: Vec<&str> = info
|
||||
.lines()
|
||||
.filter(|line| line.trim().starts_with("todo__"))
|
||||
.collect();
|
||||
let mut sorted = todo_lines.clone();
|
||||
sorted.sort_unstable();
|
||||
assert_eq!(
|
||||
todo_lines, sorted,
|
||||
"expected todo__ entries to be alphabetically sorted, got: {todo_lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_info_errors_when_function_calling_disabled() {
|
||||
let app_state = {
|
||||
let config = AppConfig {
|
||||
function_calling_support: false,
|
||||
..AppConfig::default()
|
||||
};
|
||||
Arc::new(AppState {
|
||||
config: Arc::new(config),
|
||||
vault: Arc::new(Vault::default()),
|
||||
mcp_factory: Arc::new(McpFactory::default()),
|
||||
rag_cache: Arc::new(RagCache::default()),
|
||||
mcp_config: None,
|
||||
mcp_log_path: None,
|
||||
mcp_registry: None,
|
||||
functions: Functions::default(),
|
||||
})
|
||||
};
|
||||
let ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
|
||||
let err = ctx.tools_info().unwrap_err();
|
||||
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("Function calling is disabled"),
|
||||
"expected error to mention function calling, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_info_errors_when_no_role() {
|
||||
let ctx = create_test_ctx();
|
||||
|
||||
+12
-3
@@ -49,10 +49,15 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
|
||||
4. Continue with the next pending item now. Call tools immediately."
|
||||
};
|
||||
|
||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 44]> = LazyLock::new(|| {
|
||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 45]> = LazyLock::new(|| {
|
||||
[
|
||||
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
||||
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
||||
ReplCommand::new(
|
||||
".info tools",
|
||||
"Show the list of enabled tools to be passed to the LLM",
|
||||
AssertState::True(StateFlags::FUNCTION_CALLING),
|
||||
),
|
||||
ReplCommand::new(
|
||||
".authenticate",
|
||||
"Authenticate the current model client via OAuth (if configured)",
|
||||
@@ -480,6 +485,10 @@ pub async fn run_repl_command(
|
||||
let info = ctx.agent_info()?;
|
||||
print!("{info}");
|
||||
}
|
||||
Some("tools") => {
|
||||
let info = ctx.tools_info()?;
|
||||
print!("{info}");
|
||||
}
|
||||
Some(_) => unknown_command()?,
|
||||
None => {
|
||||
let app = Arc::clone(&ctx.app.config);
|
||||
@@ -1382,8 +1391,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_commands_has_44_entries() {
|
||||
assert_eq!(REPL_COMMANDS.len(), 44);
|
||||
fn repl_commands_has_45_entries() {
|
||||
assert_eq!(REPL_COMMANDS.len(), 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user