badb97a01e
Signed-off-by: Douwe Osinga <douwe@squareup.com> Signed-off-by: Aaron Yourk <ayourk@gmail.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
258 lines
8.8 KiB
Rust
258 lines
8.8 KiB
Rust
use std::sync::Arc;
|
|
|
|
use crate::agents::platform_extensions::developer;
|
|
use crate::agents::ExtensionConfig;
|
|
use crate::config::Config;
|
|
use crate::conversation::message::Message;
|
|
use crate::model::ModelConfig;
|
|
use crate::providers::base::Provider;
|
|
use crate::providers::{self, errors::ProviderError};
|
|
use crate::session::{
|
|
config_path, latest_llm_log_path, latest_server_log_path, read_capped, read_tail, SystemInfo,
|
|
};
|
|
|
|
pub async fn run(agent: &crate::agents::Agent, session_id: &str) -> anyhow::Result<Message> {
|
|
if let Some(msg) = ensure_working_provider(agent, session_id).await? {
|
|
return Ok(msg);
|
|
}
|
|
|
|
ensure_developer_extension(agent, session_id).await;
|
|
|
|
let info = SystemInfo::collect();
|
|
let extensions = agent.list_extensions().await;
|
|
|
|
let mut prompt = format!(
|
|
"I ran /doctor because something seems off. Here's my system info:\n\n\
|
|
{}\n\
|
|
Loaded extensions: {}\n\
|
|
Config file: {}\n",
|
|
info.to_text(),
|
|
if extensions.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
extensions.join(", ")
|
|
},
|
|
config_path().display(),
|
|
);
|
|
|
|
if let Some(path) = latest_server_log_path() {
|
|
if let Some(tail) = read_tail(&path, 50) {
|
|
prompt.push_str(&format!("\nRecent server log:\n```\n{}\n```\n", tail));
|
|
}
|
|
}
|
|
|
|
if let Some(path) = latest_llm_log_path() {
|
|
if let Some(content) = read_capped(&path, 10_000) {
|
|
prompt.push_str(&format!("\nLast LLM request log:\n```\n{}\n```\n", content));
|
|
}
|
|
}
|
|
|
|
prompt.push_str(
|
|
"\nUse your tools to investigate what might be wrong. \
|
|
Check if common developer tools are available (git, etc.) \
|
|
and report what you find.",
|
|
);
|
|
|
|
Ok(Message::user().with_text(prompt))
|
|
}
|
|
|
|
async fn ensure_working_provider(
|
|
agent: &crate::agents::Agent,
|
|
session_id: &str,
|
|
) -> anyhow::Result<Option<Message>> {
|
|
let config = Config::global();
|
|
let mut log: Vec<String> = Vec::new();
|
|
|
|
let provider_name = config.get_goose_provider().ok();
|
|
let model_name = config.get_goose_model().ok();
|
|
|
|
if let (Some(ref pname), Some(ref mname)) = (&provider_name, &model_name) {
|
|
log.push(format!("Checking {} / {} ...", pname, mname));
|
|
match try_create_and_test(pname, mname).await {
|
|
Ok(_) => {
|
|
return Ok(None);
|
|
}
|
|
Err(e) => {
|
|
log.push(format!("❌ {} / {}: {}", pname, mname, describe_error(&e)));
|
|
}
|
|
}
|
|
|
|
log.push(format!("Looking for alternative models on {} ...", pname));
|
|
if let Some(working) = try_other_models(pname, mname, &mut log).await {
|
|
let new_model = working.get_model_config().model_name.clone();
|
|
save_and_set(agent, session_id, working).await?;
|
|
let preamble = log.join("\n");
|
|
return Ok(Some(Message::assistant().with_text(format!(
|
|
"**Goose Doctor**\n\n{}\n\n\
|
|
Your configured model wasn't working, so I switched to \
|
|
**{} / {}**. You can continue chatting now.",
|
|
preamble, pname, new_model,
|
|
))));
|
|
}
|
|
} else {
|
|
log.push("No provider/model configured.".to_string());
|
|
}
|
|
|
|
log.push("Looking for other configured providers ...".to_string());
|
|
let skip = provider_name.as_deref().unwrap_or("");
|
|
if let Some(working) = try_other_providers(skip, &mut log).await {
|
|
let name = working.get_name().to_string();
|
|
let model = working.get_model_config().model_name.clone();
|
|
save_and_set(agent, session_id, working).await?;
|
|
let preamble = log.join("\n");
|
|
return Ok(Some(Message::assistant().with_text(format!(
|
|
"**Goose Doctor**\n\n{}\n\n\
|
|
Switched to **{} / {}**. You can continue chatting now.",
|
|
preamble, name, model,
|
|
))));
|
|
}
|
|
|
|
let preamble = log.join("\n");
|
|
Ok(Some(Message::assistant().with_text(format!(
|
|
"**Goose Doctor**\n\n{}\n\n\
|
|
No working provider found. Run `goose configure` to set one up.",
|
|
preamble,
|
|
))))
|
|
}
|
|
|
|
async fn ensure_developer_extension(agent: &crate::agents::Agent, session_id: &str) {
|
|
if agent
|
|
.extension_manager
|
|
.is_extension_enabled(developer::EXTENSION_NAME)
|
|
.await
|
|
{
|
|
return;
|
|
}
|
|
let config = ExtensionConfig::Platform {
|
|
name: developer::EXTENSION_NAME.to_string(),
|
|
description: "Write and edit files, and execute shell commands".to_string(),
|
|
display_name: Some("Developer".to_string()),
|
|
bundled: None,
|
|
available_tools: vec![],
|
|
};
|
|
if let Err(e) = agent.add_extension(config, session_id).await {
|
|
tracing::warn!("Doctor: failed to load developer extension: {}", e);
|
|
}
|
|
}
|
|
|
|
async fn save_and_set(
|
|
agent: &crate::agents::Agent,
|
|
session_id: &str,
|
|
provider: Arc<dyn Provider>,
|
|
) -> anyhow::Result<()> {
|
|
let config = Config::global();
|
|
crate::config::set_active_provider(
|
|
config,
|
|
provider.get_name(),
|
|
&provider.get_model_config().model_name,
|
|
)?;
|
|
agent.update_provider(provider, session_id).await
|
|
}
|
|
|
|
async fn test_provider(provider: &dyn Provider) -> Result<(), ProviderError> {
|
|
let messages = vec![Message::user().with_text("Say 'hello' and nothing else.")];
|
|
provider
|
|
.complete(
|
|
&provider.get_model_config(),
|
|
"doctor-check",
|
|
"Respond as briefly as possible.",
|
|
&messages,
|
|
&[],
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn try_create_and_test(
|
|
provider_name: &str,
|
|
model_name: &str,
|
|
) -> Result<Arc<dyn Provider>, ProviderError> {
|
|
let model_config = ModelConfig::new(model_name)
|
|
.map_err(|e| ProviderError::ExecutionError(e.to_string()))?
|
|
.with_canonical_limits(provider_name);
|
|
|
|
let provider = providers::create(provider_name, model_config, vec![])
|
|
.await
|
|
.map_err(|e| ProviderError::ExecutionError(e.to_string()))?;
|
|
|
|
test_provider(provider.as_ref()).await?;
|
|
Ok(provider)
|
|
}
|
|
|
|
async fn try_other_models(
|
|
provider_name: &str,
|
|
skip_model: &str,
|
|
log: &mut Vec<String>,
|
|
) -> Option<Arc<dyn Provider>> {
|
|
let entry = providers::get_from_registry(provider_name).await.ok()?;
|
|
let temp = entry.create_with_default_model(vec![]).await.ok()?;
|
|
let models = temp.fetch_recommended_models().await.ok()?;
|
|
|
|
for model in models.iter().filter(|m| m.as_str() != skip_model).take(3) {
|
|
log.push(format!(" Trying {} / {} ...", provider_name, model));
|
|
match try_create_and_test(provider_name, model).await {
|
|
Ok(p) => {
|
|
log.push(format!(" ✓ {} / {} works", provider_name, model));
|
|
return Some(p);
|
|
}
|
|
Err(e) => log.push(format!(" ✗ {}", describe_error(&e))),
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
async fn try_other_providers(skip: &str, log: &mut Vec<String>) -> Option<Arc<dyn Provider>> {
|
|
for (meta, _) in providers::providers().await {
|
|
if meta.name == skip {
|
|
continue;
|
|
}
|
|
let entry = match providers::get_from_registry(&meta.name).await {
|
|
Ok(e) => e,
|
|
Err(_) => continue,
|
|
};
|
|
let provider = match entry.create_with_default_model(vec![]).await {
|
|
Ok(p) => p,
|
|
Err(_) => continue,
|
|
};
|
|
let model_name = provider.get_model_config().model_name.clone();
|
|
log.push(format!(" Trying {} / {} ...", meta.name, model_name));
|
|
match test_provider(provider.as_ref()).await {
|
|
Ok(()) => {
|
|
log.push(format!(" ✓ {} / {} works", meta.name, model_name));
|
|
return Some(provider);
|
|
}
|
|
Err(e) => log.push(format!(" ✗ {}", describe_error(&e))),
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn describe_error(e: &ProviderError) -> String {
|
|
match e {
|
|
ProviderError::Authentication(_) => {
|
|
"Authentication failed — check your API key. Run `goose configure` to update it."
|
|
.to_string()
|
|
}
|
|
ProviderError::CreditsExhausted { top_up_url, .. } => {
|
|
let mut msg = "Credits exhausted.".to_string();
|
|
if let Some(url) = top_up_url {
|
|
msg.push_str(&format!(" Top up at: {}", url));
|
|
}
|
|
msg
|
|
}
|
|
ProviderError::RateLimitExceeded { .. } => {
|
|
"Rate limited — wait a moment and try again.".to_string()
|
|
}
|
|
ProviderError::EndpointNotFound(_) => {
|
|
"Model not found — the model name may be wrong for this provider.".to_string()
|
|
}
|
|
ProviderError::NetworkError(_) => {
|
|
"Network error — check your internet connection.".to_string()
|
|
}
|
|
ProviderError::ServerError(_) => {
|
|
"Provider server error — the service may be temporarily down.".to_string()
|
|
}
|
|
other => format!("{}", other),
|
|
}
|
|
}
|