Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] main from block:main #7

Merged
merged 6 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,6 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
.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...");
Expand Down Expand Up @@ -319,6 +315,9 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {

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)
}
Expand Down
114 changes: 105 additions & 9 deletions crates/goose-mcp/src/developer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<String, Prompt> {
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<Tool>,
file_history: Arc<Mutex<HashMap<PathBuf, Vec<String>>>>,
prompts: Arc<HashMap<String, Prompt>>,
instructions: String,
file_history: Arc<Mutex<HashMap<PathBuf, Vec<String>>>>,
}

impl Default for DeveloperRouter {
Expand Down Expand Up @@ -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())),
}
}

Expand Down Expand Up @@ -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<Tool> {
Expand Down Expand Up @@ -762,14 +814,58 @@ impl Router for DeveloperRouter {
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
Box::pin(async move { Ok("".to_string()) })
}

fn list_prompts(&self) -> Option<Vec<Prompt>> {
if self.prompts.is_empty() {
None
} else {
Some(self.prompts.values().cloned().collect())
}
}

fn get_prompt(
&self,
prompt_name: &str,
) -> Option<Pin<Box<dyn Future<Output = Result<String, PromptError>> + 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),
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions crates/goose-mcp/src/developer/prompts/unit_test.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
9 changes: 7 additions & 2 deletions crates/goose/src/agents/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExtensionInfo>> = HashMap::new();
let mut context: HashMap<&str, Value> = HashMap::new();

let extensions_info: Vec<ExtensionInfo> = self
.clients
.keys()
Expand All @@ -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")
}

Expand Down
28 changes: 21 additions & 7 deletions crates/goose/src/prompts/system.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
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
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,
Expand All @@ -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 %}
{% 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., <http://example.com/>).
- 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.
Binary file added extensions-site/app/assets/favicon.ico
Binary file not shown.
8 changes: 6 additions & 2 deletions extensions-site/app/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ 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 = () => {
const location = useLocation();
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 (
<div className="bg-bgApp container mx-auto border-borderSubtle py-16">
<div className="h-full flex justify-between items-center">
<NavLink to="/" className="text-textProminent">
<NavLink to={gooseIconLink} className="text-textProminent">
<IconGoose />
</NavLink>
<div className="w-auto items-center flex">
Expand Down
3 changes: 3 additions & 0 deletions extensions-site/app/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const SiteURLs = {
GOOSE_HOMEPAGE: "https://block.github.io/goose/",
};
2 changes: 2 additions & 0 deletions extensions-site/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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 }) {
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/src/components/LoadingGoose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const LoadingGoose = () => {
<div className="w-full pb-[2px]">
<div className="flex items-center text-xs text-textStandard mb-2 mt-2 pl-4 animate-[appear_250ms_ease-in_forwards]">
<GooseLogo className="mr-2" size="small" hover={false} />
goose is working on it..
goose is working on it
</div>
</div>
);
Expand Down
Loading