diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index a998a50e9..678390cc4 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -283,10 +283,6 @@ pub async fn configure_provider_dialog() -> Result> { .default_input(&default_model) .interact()?; - // Update config with new values - config.set("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?; - config.set("GOOSE_MODEL", Value::String(model.clone()))?; - // Test the configuration let spin = spinner(); spin.start("Checking your configuration..."); @@ -319,6 +315,9 @@ pub async fn configure_provider_dialog() -> Result> { match result { Ok((_message, _usage)) => { + // Update config with new values only if the test succeeds + config.set("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?; + config.set("GOOSE_MODEL", Value::String(model.clone()))?; cliclack::outro("Configuration saved successfully")?; Ok(true) } diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index a55372cb0..f85096a2d 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -15,8 +15,10 @@ use std::{ use tokio::process::Command; use url::Url; +use include_dir::{include_dir, Dir}; +use mcp_core::prompt::{Prompt, PromptArgument, PromptTemplate}; use mcp_core::{ - handler::{ResourceError, ToolError}, + handler::{PromptError, ResourceError, ToolError}, protocol::ServerCapabilities, resource::Resource, tool::Tool, @@ -27,20 +29,66 @@ use mcp_server::Router; use mcp_core::content::Content; use mcp_core::role::Role; +use self::shell::{ + expand_path, format_command_for_platform, get_shell_config, is_absolute_path, + normalize_line_endings, +}; use indoc::indoc; use std::process::Stdio; use std::sync::{Arc, Mutex}; use xcap::{Monitor, Window}; -use self::shell::{ - expand_path, format_command_for_platform, get_shell_config, is_absolute_path, - normalize_line_endings, -}; +// Embeds the prompts directory to the build +static PROMPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/developer/prompts"); + +/// Loads prompt files from the embedded PROMPTS_DIR and returns a HashMap of prompts. +/// Ensures that each prompt name is unique. +pub fn load_prompt_files() -> HashMap { + let mut prompts = HashMap::new(); + + for entry in PROMPTS_DIR.files() { + let prompt_str = String::from_utf8_lossy(entry.contents()).into_owned(); + + let template: PromptTemplate = match serde_json::from_str(&prompt_str) { + Ok(t) => t, + Err(e) => { + eprintln!( + "Failed to parse prompt template in {}: {}", + entry.path().display(), + e + ); + continue; // Skip invalid prompt file + } + }; + + let arguments = template + .arguments + .into_iter() + .map(|arg| PromptArgument { + name: arg.name, + description: arg.description, + required: arg.required, + }) + .collect(); + + let prompt = Prompt::new(&template.id, &template.template, arguments); + + if prompts.contains_key(&prompt.name) { + eprintln!("Duplicate prompt name '{}' found. Skipping.", prompt.name); + continue; // Skip duplicate prompt name + } + + prompts.insert(prompt.name.clone(), prompt); + } + + prompts +} pub struct DeveloperRouter { tools: Vec, - file_history: Arc>>>, + prompts: Arc>, instructions: String, + file_history: Arc>>>, } impl Default for DeveloperRouter { @@ -273,8 +321,9 @@ impl DeveloperRouter { list_windows_tool, screen_capture_tool, ], - file_history: Arc::new(Mutex::new(HashMap::new())), + prompts: Arc::new(load_prompt_files()), instructions, + file_history: Arc::new(Mutex::new(HashMap::new())), } } @@ -726,7 +775,10 @@ impl Router for DeveloperRouter { } fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new().with_tools(false).build() + CapabilitiesBuilder::new() + .with_tools(false) + .with_prompts(false) + .build() } fn list_tools(&self) -> Vec { @@ -762,14 +814,58 @@ impl Router for DeveloperRouter { ) -> Pin> + Send + 'static>> { Box::pin(async move { Ok("".to_string()) }) } + + fn list_prompts(&self) -> Option> { + if self.prompts.is_empty() { + None + } else { + Some(self.prompts.values().cloned().collect()) + } + } + + fn get_prompt( + &self, + prompt_name: &str, + ) -> Option> + Send + 'static>>> { + let prompt_name = prompt_name.trim().to_owned(); + + // Validate prompt name is not empty + if prompt_name.is_empty() { + return Some(Box::pin(async move { + Err(PromptError::InvalidParameters( + "Prompt name cannot be empty".to_string(), + )) + })); + } + + let prompts = Arc::clone(&self.prompts); + + Some(Box::pin(async move { + match prompts.get(&prompt_name) { + Some(prompt) => { + if prompt.description.trim().is_empty() { + Err(PromptError::InternalError(format!( + "Prompt '{prompt_name}' has an empty description" + ))) + } else { + Ok(prompt.description.clone()) + } + } + None => Err(PromptError::NotFound(format!( + "Prompt '{prompt_name}' not found" + ))), + } + })) + } } impl Clone for DeveloperRouter { fn clone(&self) -> Self { Self { tools: self.tools.clone(), - file_history: Arc::clone(&self.file_history), + prompts: Arc::clone(&self.prompts), instructions: self.instructions.clone(), + file_history: Arc::clone(&self.file_history), } } } diff --git a/crates/goose-mcp/src/developer/prompts/unit_test.json b/crates/goose-mcp/src/developer/prompts/unit_test.json new file mode 100644 index 000000000..abef9414f --- /dev/null +++ b/crates/goose-mcp/src/developer/prompts/unit_test.json @@ -0,0 +1,16 @@ +{ + "id": "unit_test", + "template": "Generate or update unit tests for a given source code file.\n\nThe source code file is provided in {source_code}.\nPlease update the existing tests, ensure they are passing, and add any new tests as needed.\n\nThe test suite should:\n- Follow language-specific test naming conventions for {language}\n- Include all necessary imports and annotations\n- Thoroughly test the specified functionality\n- Ensure tests are passing before completion\n- Handle edge cases and error conditions\n- Use clear test names that reflect what is being tested", + "arguments": [ + { + "name": "source_code", + "description": "The source code file content to be tested", + "required": true + }, + { + "name": "language", + "description": "The programming language of the source code", + "required": true + } + ] + } \ No newline at end of file diff --git a/crates/goose/src/agents/capabilities.rs b/crates/goose/src/agents/capabilities.rs index 9a01a6e9c..7fcd6d20e 100644 --- a/crates/goose/src/agents/capabilities.rs +++ b/crates/goose/src/agents/capabilities.rs @@ -286,7 +286,8 @@ impl Capabilities { /// Get the extension prompt including client instructions pub async fn get_system_prompt(&self) -> String { - let mut context: HashMap<&str, Vec> = HashMap::new(); + let mut context: HashMap<&str, Value> = HashMap::new(); + let extensions_info: Vec = self .clients .keys() @@ -297,7 +298,11 @@ impl Capabilities { }) .collect(); - context.insert("extensions", extensions_info); + let current_date_time = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); + + context.insert("extensions", serde_json::to_value(extensions_info).unwrap()); + context.insert("current_date_time", Value::String(current_date_time)); + load_prompt_file("system.md", &context).expect("Prompt should render") } diff --git a/crates/goose/src/prompts/system.md b/crates/goose/src/prompts/system.md index 97ac8dee9..d7f5db88b 100644 --- a/crates/goose/src/prompts/system.md +++ b/crates/goose/src/prompts/system.md @@ -1,8 +1,14 @@ -You are a general purpose AI agent called Goose. You are capable -of dynamically plugging into new extensions and learning how to use them. +You are a general-purpose AI agent called Goose, created by Block, the parent company of Square, CashApp, and Tidal. Goose is being developed as an open-source software project. -You solve higher level problems using the tools in these extensions, and can -interact with multiple at once. +The current date is {{current_date_time}}. + +Goose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o, claude-3.5-sonnet, o1, llama-3.2, deepseek-r1, etc). +These models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10 months prior to the current date. + +# Extensions + +Extensions allow other applications to provide context to Goose. Extensions connect Goose to different data sources and tools. +You are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level problems using the tools in these extensions, and can interact with multiple at once. {% if (extensions is defined) and extensions %} Because you dynamically load extensions, your conversation history may refer @@ -10,9 +16,7 @@ to interactions with extensions that are not currently active. The currently active extensions are below. Each of these extensions provides tools that are in your tool specification. -# Extensions: {% for extension in extensions %} - ## {{extension.name}} {% if extension.has_resources %} {{extension.name}} supports resources, you can use platform__read_resource, @@ -24,4 +28,14 @@ and platform__list_resources on this extension. {% else %} No extensions are defined. You should let the user know that they should add extensions. -{% endif %} \ No newline at end of file +{% endif %} + +# Response Guidelines + +- Use Markdown formatting for all responses. +- Follow best practices for Markdown, including: + - Using headers for organization. + - Bullet points for lists. + - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic links using angle brackets (e.g., ). +- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting. +- Ensure clarity, conciseness, and proper formatting to enhance readability and usability. diff --git a/extensions-site/app/assets/favicon.ico b/extensions-site/app/assets/favicon.ico new file mode 100644 index 000000000..29e3cfdc3 Binary files /dev/null and b/extensions-site/app/assets/favicon.ico differ diff --git a/extensions-site/app/components/header.tsx b/extensions-site/app/components/header.tsx index 06f5b9353..8770beb06 100644 --- a/extensions-site/app/components/header.tsx +++ b/extensions-site/app/components/header.tsx @@ -2,6 +2,7 @@ import { IconDownload } from "./icons/download"; import { IconGoose } from "./icons/goose"; import { ThemeToggle } from "./themeToggle"; import { Button } from "./ui/button"; +import { SiteURLs } from '../constants'; import { NavLink, useLocation } from "react-router"; export const Header = () => { @@ -9,11 +10,14 @@ export const Header = () => { const { hash, pathname, search } = location; const stableDownload = "https://github.com/block/goose/releases/download/stable/Goose.zip"; - + + // link back to the main site if the icon is clicked on the extensions homepage + // otherwise link back to the extensions homepage + const gooseIconLink = pathname === "/" ? SiteURLs.GOOSE_HOMEPAGE : "/"; return (
- +
diff --git a/extensions-site/app/constants.ts b/extensions-site/app/constants.ts new file mode 100644 index 000000000..319d5279a --- /dev/null +++ b/extensions-site/app/constants.ts @@ -0,0 +1,3 @@ +export const SiteURLs = { + GOOSE_HOMEPAGE: "https://block.github.io/goose/", +}; diff --git a/extensions-site/app/root.tsx b/extensions-site/app/root.tsx index da7cdfc27..1e073fb3d 100644 --- a/extensions-site/app/root.tsx +++ b/extensions-site/app/root.tsx @@ -10,6 +10,7 @@ import { import type { Route } from "./+types/root"; import stylesheet from "./styles/main.css?url"; import { Header } from "./components/header"; +import favicon from "./assets/favicon.ico?url"; export const links: Route.LinksFunction = () => [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, @@ -23,6 +24,7 @@ export const links: Route.LinksFunction = () => [ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", }, { rel: "stylesheet", href: stylesheet }, + { rel: "icon", type: "image/x-icon", href: favicon }, ]; export function Layout({ children }: { children: React.ReactNode }) { diff --git a/ui/desktop/src/components/LoadingGoose.tsx b/ui/desktop/src/components/LoadingGoose.tsx index d9679d334..ba9e557d8 100644 --- a/ui/desktop/src/components/LoadingGoose.tsx +++ b/ui/desktop/src/components/LoadingGoose.tsx @@ -6,7 +6,7 @@ const LoadingGoose = () => {
- goose is working on it.. + goose is working on it…
);