feat: Implement a simplified reference agent and dev toolkit (#564)

This commit is contained in:
Bradley Axen
2025-01-08 19:25:51 -08:00
committed by GitHub
parent c3cf4eec37
commit 32c7a3df95
10 changed files with 771 additions and 2 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
use anyhow::Result;
use goose_mcp::NonDeveloperRouter;
use goose_mcp::{DeveloperRouter, JetBrainsRouter};
use goose_mcp::{Developer2Router, DeveloperRouter, JetBrainsRouter};
use mcp_server::router::RouterService;
use mcp_server::{BoundedService, ByteTransport, Server};
use tokio::io::{stdin, stdout};
@@ -10,6 +10,7 @@ pub async fn run_server(name: &str) -> Result<()> {
let router: Option<Box<dyn BoundedService>> = match name {
"developer" => Some(Box::new(RouterService(DeveloperRouter::new()))),
"developer2" => Some(Box::new(RouterService(Developer2Router::new()))),
"nondeveloper" => Some(Box::new(RouterService(NonDeveloperRouter::new()))),
"jetbrains" => Some(Box::new(RouterService(JetBrainsRouter::new()))),
_ => None,
+3 -1
View File
@@ -55,13 +55,15 @@ pub async fn build_session<'a>(
// TODO use systems from the profile
// TODO once the client/server for MCP has stabilized, we should probably add InProcess transport to each
// and avoid spawning here. But it is at least included in the CLI for portability
let system = std::env::var("GOOSE_SYSTEM").unwrap_or("developer".to_string());
let config = SystemConfig::stdio(
std::env::current_exe()
.expect("should find the current executable")
.to_str()
.expect("should resolve executable to string path"),
)
.with_args(vec!["mcp", "developer"]);
.with_args(vec!["mcp", &system]);
agent
.add_system(config)
.await
+34
View File
@@ -0,0 +1,34 @@
use std::path::Path;
/// Get the markdown language identifier for a file extension
pub fn get_language_identifier(path: &Path) -> &'static str {
match path.extension().and_then(|ext| ext.to_str()) {
Some("rs") => "rust",
Some("py") => "python",
Some("js") => "javascript",
Some("ts") => "typescript",
Some("json") => "json",
Some("toml") => "toml",
Some("yaml") | Some("yml") => "yaml",
Some("sh") => "bash",
Some("go") => "go",
Some("md") => "markdown",
Some("html") => "html",
Some("css") => "css",
Some("sql") => "sql",
Some("java") => "java",
Some("cpp") | Some("cc") | Some("cxx") => "cpp",
Some("c") => "c",
Some("h") | Some("hpp") => "cpp",
Some("rb") => "ruby",
Some("php") => "php",
Some("swift") => "swift",
Some("kt") | Some("kts") => "kotlin",
Some("scala") => "scala",
Some("r") => "r",
Some("m") => "matlab",
Some("pl") => "perl",
Some("dockerfile") => "dockerfile",
_ => "",
}
}
+530
View File
@@ -0,0 +1,530 @@
mod lang;
use anyhow::Result;
use indoc::formatdoc;
use serde_json::{json, Value};
use std::{
collections::HashMap,
future::Future,
path::{Path, PathBuf},
pin::Pin,
};
use tokio::process::Command;
use url::Url;
use mcp_core::{
handler::{ResourceError, ToolError},
protocol::ServerCapabilities,
resource::Resource,
tool::Tool,
};
use mcp_server::router::CapabilitiesBuilder;
use mcp_server::Router;
use mcp_core::content::Content;
use mcp_core::role::Role;
use indoc::indoc;
use std::process::Stdio;
use std::sync::{Arc, Mutex};
pub struct Developer2Router {
tools: Vec<Tool>,
file_history: Arc<Mutex<HashMap<PathBuf, Vec<String>>>>,
instructions: String,
}
impl Default for Developer2Router {
fn default() -> Self {
Self::new()
}
}
impl Developer2Router {
pub fn new() -> Self {
// TODO consider rust native search tools, we could use
// https://docs.rs/ignore/latest/ignore/
let bash_tool = Tool::new(
"shell".to_string(),
indoc! {r#"
Execute a command in the shell.
This will return the output and error concatenated into a single string, as
you would see from running on the command line. There will also be an indication
of if the command succeeded or failed.
Avoid commands that produce a large amount of ouput, and consider piping those outputs to files.
If you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that
this tool does not run indefinitely.
**Important**: Use ripgrep - `rg` - when you need to locate a file or a code reference, other solutions
may show ignored or hidden files. For example *do not* use `find` or `ls -r`
- To locate a file by name: `rg --files | rg example.py`
- To locate consent inside files: `rg 'class Example'`
"#}.to_string(),
json!({
"type": "object",
"required": ["command"],
"properties": {
"command": {"type": "string"}
}
}),
);
let text_editor_tool = Tool::new(
"text_editor".to_string(),
indoc! {r#"
Perform text editing operations on files.
The `command` parameter specifies the operation to perform. Allowed options are:
- `view`: View the content of a file.
- `create`: Create a new file with the given content (it will fail if the file already exists)
- `str_replace`: Replace a string in a file with a new string.
- `undo_edit`: Undo the last edit made to a file.
To use the create command, you must specify `file_text` which will become the content of the new file.
To use the str_replace command, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one
unique section of the original file, including any whitespace. Make sure to include enough context that the match is not
ambiguous. The entire original string will be replaced with `new_str`.
"#}.to_string(),
json!({
"type": "object",
"required": ["command", "path"],
"properties": {
"path": {
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.",
"type": "string"
},
"command": {
"type": "string",
"enum": ["view", "create", "str_replace", "undo_edit"],
"description": "Allowed options are: `view`, `create`, `str_replace`, undo_edit`."
},
"old_str": {"type": "string"},
"new_str": {"type": "string"},
"file_text": {"type": "string"}
}
}),
);
let instructions = formatdoc! {r#"
The developer system gives you the capabilities to edit code files and run shell commands,
and can be used to solve a wide range of problems.
You can use the shell tool to run any command that would work on the relevant operating system.
Use the shell tool as needed to locate files or interact with the project.
operating system: {os}
current directory: {cwd}
"#,
os=std::env::consts::OS,
cwd=std::env::current_dir().expect("should have a current working dir").to_string_lossy(),
};
Self {
tools: vec![bash_tool, text_editor_tool],
file_history: Arc::new(Mutex::new(HashMap::new())),
instructions,
}
}
// Helper method to resolve a path relative to cwd
fn resolve_path(&self, path_str: &str) -> Result<PathBuf, ToolError> {
let cwd = std::env::current_dir().expect("should have a current working dir");
let expanded = shellexpand::tilde(path_str);
let path = Path::new(expanded.as_ref());
let suggestion = cwd.join(path);
match path.is_absolute() {
true => Ok(path.to_path_buf()),
false => Err(ToolError::InvalidParameters(format!(
"The path {} is not an absolute path, did you possibly mean {}?",
path_str,
suggestion.to_string_lossy(),
))),
}
}
// Implement bash tool functionality
async fn bash(&self, params: Value) -> Result<Vec<Content>, ToolError> {
let command =
params
.get("command")
.and_then(|v| v.as_str())
.ok_or(ToolError::InvalidParameters(
"The command string is required".to_string(),
))?;
// TODO consider command suggestions and safety rails
// TODO be more careful about backgrounding, revisit interleave
// Redirect stderr to stdout to interleave outputs
let cmd_with_redirect = format!("{} 2>&1", command);
// Execute the command
let child = Command::new("bash")
.stdout(Stdio::piped()) // These two pipes required to capture output later.
.stderr(Stdio::piped())
.kill_on_drop(true) // Critical so that the command is killed when the agent.reply stream is interrupted.
.arg("-c")
.arg(cmd_with_redirect)
.spawn()
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
// Wait for the command to complete and get output
let output = child
.wait_with_output()
.await
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let output_str = String::from_utf8_lossy(&output.stdout);
Ok(vec![
Content::text(output_str.clone()).with_audience(vec![Role::Assistant]),
Content::text(output_str)
.with_audience(vec![Role::User])
.with_priority(0.0),
])
}
async fn text_editor(&self, params: Value) -> Result<Vec<Content>, ToolError> {
let command = params
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'command' parameter".to_string())
})?;
let path_str = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'path' parameter".into()))?;
let path = self.resolve_path(path_str)?;
match command {
"view" => self.text_editor_view(&path).await,
"create" => {
let file_text = params
.get("file_text")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'file_text' parameter".into())
})?;
self.text_editor_create(&path, file_text).await
}
"str_replace" => {
let old_str = params
.get("old_str")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'old_str' parameter".into())
})?;
let new_str = params
.get("new_str")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'new_str' parameter".into())
})?;
self.text_editor_replace(&path, old_str, new_str).await
}
"undo_edit" => self.text_editor_undo(&path).await,
_ => Err(ToolError::InvalidParameters(format!(
"Unknown command '{}'",
command
))),
}
}
async fn text_editor_view(&self, path: &PathBuf) -> Result<Vec<Content>, ToolError> {
if path.is_file() {
// Check file size first (2MB limit)
const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024; // 2MB in bytes
const MAX_CHAR_COUNT: usize = 1 << 20; // 2^20 characters (1,048,576)
let file_size = std::fs::metadata(path)
.map_err(|e| {
ToolError::ExecutionError(format!("Failed to get file metadata: {}", e))
})?
.len();
if file_size > MAX_FILE_SIZE {
return Err(ToolError::ExecutionError(format!(
"File '{}' is too large ({:.2}MB). Maximum size is 2MB to prevent memory issues.",
path.display(),
file_size as f64 / 1024.0 / 1024.0
)));
}
let uri = Url::from_file_path(path)
.map_err(|_| ToolError::ExecutionError("Invalid file path".into()))?
.to_string();
let content = std::fs::read_to_string(path)
.map_err(|e| ToolError::ExecutionError(format!("Failed to read file: {}", e)))?;
let char_count = content.chars().count();
if char_count > MAX_CHAR_COUNT {
return Err(ToolError::ExecutionError(format!(
"File '{}' has too many characters ({}). Maximum character count is {}.",
path.display(),
char_count,
MAX_CHAR_COUNT
)));
}
let language = lang::get_language_identifier(path);
let formatted = formatdoc! {"
### {path}
```{language}
{content}
```
",
path=path.display(),
language=language,
content=content,
};
// The LLM gets just a quick update as we expect the file to view in the status
// but we send a low priority message for the human
Ok(vec![
Content::embedded_text(uri, content).with_audience(vec![Role::Assistant]),
Content::text(formatted)
.with_audience(vec![Role::User])
.with_priority(0.0),
])
} else {
Err(ToolError::ExecutionError(format!(
"The path '{}' does not exist or is not a file.",
path.display()
)))
}
}
async fn text_editor_create(
&self,
path: &PathBuf,
file_text: &str,
) -> Result<Vec<Content>, ToolError> {
// Check if file already exists
if path.exists() {
return Err(ToolError::InvalidParameters(format!(
"File '{}' already exists - you will need to edit it with the `str_replace` command",
path.display()
)));
}
// Write to the file
std::fs::write(path, file_text)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write file: {}", e)))?;
// Try to detect the language from the file extension
let language = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
// The assistant output does not show the file again because the content is already in the tool request
// but we do show it to the user here
Ok(vec![
Content::text(format!("Successfully wrote to {}", path.display()))
.with_audience(vec![Role::Assistant]),
Content::text(formatdoc! {r#"
### {path}
```{language}
{content}
```
"#,
path=path.display(),
language=language,
content=file_text,
})
.with_audience(vec![Role::User])
.with_priority(0.2),
])
}
async fn text_editor_replace(
&self,
path: &PathBuf,
old_str: &str,
new_str: &str,
) -> Result<Vec<Content>, ToolError> {
// Check if file exists and is active
if !path.exists() {
return Err(ToolError::InvalidParameters(format!(
"File '{}' does not exist, you can write a new file with the `create` command",
path.display()
)));
}
// Read content
let content = std::fs::read_to_string(path)
.map_err(|e| ToolError::ExecutionError(format!("Failed to read file: {}", e)))?;
// Ensure 'old_str' appears exactly once
if content.matches(old_str).count() > 1 {
return Err(ToolError::InvalidParameters(
"'old_str' must appear exactly once in the file, but it appears multiple times"
.into(),
));
}
if content.matches(old_str).count() == 0 {
return Err(ToolError::InvalidParameters(
"'old_str' must appear exactly once in the file, but it does not appear in the file. Make sure the string exactly matches existing file content, including whitespace!".into(),
));
}
// Save history for undo
self.save_file_history(path)?;
// Replace and write back
let new_content = content.replace(old_str, new_str);
std::fs::write(path, &new_content)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write file: {}", e)))?;
// Try to detect the language from the file extension
let language = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
// Show a snippet of the changed content with context
const SNIPPET_LINES: usize = 4;
// Count newlines before the replacement to find the line number
let replacement_line = content
.split(old_str)
.next()
.expect("should split on already matched content")
.matches('\n')
.count();
// Calculate start and end lines for the snippet
let start_line = replacement_line.saturating_sub(SNIPPET_LINES);
let end_line = replacement_line + SNIPPET_LINES + new_str.matches('\n').count();
// Get the relevant lines for our snippet
let lines: Vec<&str> = new_content.lines().collect();
let snippet = lines
.iter()
.skip(start_line)
.take(end_line - start_line + 1)
.cloned()
.collect::<Vec<&str>>()
.join("\n");
let output = formatdoc! {r#"
```{language}
{snippet}
```
"#,
language=language,
snippet=snippet
};
let success_message = formatdoc! {r#"
The file {} has been edited, and the section now reads:
{}
Review the changes above for errors. Undo and edit the file again if necessary!
"#,
path.display(),
output
};
Ok(vec![
Content::text(success_message).with_audience(vec![Role::Assistant]),
Content::text(output)
.with_audience(vec![Role::User])
.with_priority(0.2),
])
}
async fn text_editor_undo(&self, path: &PathBuf) -> Result<Vec<Content>, ToolError> {
let mut history = self.file_history.lock().unwrap();
if let Some(contents) = history.get_mut(path) {
if let Some(previous_content) = contents.pop() {
// Write previous content back to file
std::fs::write(path, previous_content).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write file: {}", e))
})?;
Ok(vec![Content::text("Undid the last edit")])
} else {
Err(ToolError::InvalidParameters(
"No edit history available to undo".into(),
))
}
} else {
Err(ToolError::InvalidParameters(
"No edit history available to undo".into(),
))
}
}
fn save_file_history(&self, path: &PathBuf) -> Result<(), ToolError> {
let mut history = self.file_history.lock().unwrap();
let content = if path.exists() {
std::fs::read_to_string(path)
.map_err(|e| ToolError::ExecutionError(format!("Failed to read file: {}", e)))?
} else {
String::new()
};
history.entry(path.clone()).or_default().push(content);
Ok(())
}
}
impl Router for Developer2Router {
fn name(&self) -> String {
"developer".to_string()
}
fn instructions(&self) -> String {
self.instructions.clone()
}
fn capabilities(&self) -> ServerCapabilities {
CapabilitiesBuilder::new().with_tools(true).build()
}
fn list_tools(&self) -> Vec<Tool> {
self.tools.clone()
}
fn call_tool(
&self,
tool_name: &str,
arguments: Value,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
Box::pin(async move {
match tool_name.as_str() {
"shell" => this.bash(arguments).await,
"text_editor" => this.text_editor(arguments).await,
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
}
})
}
// TODO see if we can make it easy to skip implementing these
fn list_resources(&self) -> Vec<Resource> {
Vec::new()
}
fn read_resource(
&self,
_uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
Box::pin(async move { Ok("".to_string()) })
}
}
impl Clone for Developer2Router {
fn clone(&self) -> Self {
Self {
tools: self.tools.clone(),
file_history: Arc::clone(&self.file_history),
instructions: self.instructions.clone(),
}
}
}
+2
View File
@@ -1,7 +1,9 @@
mod developer;
mod developer2;
mod jetbrains;
mod nondeveloper;
pub use developer::DeveloperRouter;
pub use developer2::Developer2Router;
pub use jetbrains::JetBrainsRouter;
pub use nondeveloper::NonDeveloperRouter;
+1
View File
@@ -3,6 +3,7 @@ mod capabilities;
mod default;
mod factory;
mod system;
mod reference;
pub use agent::Agent;
pub use capabilities::Capabilities;
+147
View File
@@ -0,0 +1,147 @@
/// A simplified agent implementation used as a reference
/// It makes no attempt to handle context limits, and cannot read resources
use async_trait::async_trait;
use futures::stream::BoxStream;
use tokio::sync::Mutex;
use tracing::{debug, instrument};
use super::Agent;
use crate::agents::capabilities::Capabilities;
use crate::agents::system::{SystemConfig, SystemResult};
use crate::message::{Message, ToolRequest};
use crate::providers::base::Provider;
use crate::providers::base::ProviderUsage;
use crate::register_agent;
use crate::token_counter::TokenCounter;
use serde_json::Value;
/// Reference implementation of an Agent
pub struct ReferenceAgent {
capabilities: Mutex<Capabilities>,
_token_counter: TokenCounter,
}
impl ReferenceAgent {
pub fn new(provider: Box<dyn Provider>) -> Self {
Self {
capabilities: Mutex::new(Capabilities::new(provider)),
_token_counter: TokenCounter::new(),
}
}
}
#[async_trait]
impl Agent for ReferenceAgent {
async fn add_system(&mut self, system: SystemConfig) -> SystemResult<()> {
let mut capabilities = self.capabilities.lock().await;
capabilities.add_system(system).await
}
async fn remove_system(&mut self, name: &str) {
let mut capabilities = self.capabilities.lock().await;
capabilities
.remove_system(name)
.await
.expect("Failed to remove system");
}
async fn list_systems(&self) -> Vec<String> {
let capabilities = self.capabilities.lock().await;
capabilities
.list_systems()
.await
.expect("Failed to list systems")
}
async fn passthrough(&self, _system: &str, _request: Value) -> SystemResult<Value> {
// TODO implement
Ok(Value::Null)
}
#[instrument(skip(self, messages), fields(user_message))]
async fn reply(
&self,
messages: &[Message],
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
let mut messages = messages.to_vec();
let reply_span = tracing::Span::current();
let mut capabilities = self.capabilities.lock().await;
let tools = capabilities.get_prefixed_tools().await?;
let system_prompt = capabilities.get_system_prompt().await;
let _estimated_limit = capabilities
.provider()
.get_model_config()
.get_estimated_limit();
// Set the user_message field in the span instead of creating a new event
if let Some(content) = messages
.last()
.and_then(|msg| msg.content.first())
.and_then(|c| c.as_text())
{
debug!("user_message" = &content);
}
// Update conversation history for the start of the reply
let _resources = capabilities.get_resources().await?;
Ok(Box::pin(async_stream::try_stream! {
let _reply_guard = reply_span.enter();
loop {
// Get completion from provider
let (response, usage) = capabilities.provider().complete(
&system_prompt,
&messages,
&tools,
).await?;
capabilities.record_usage(usage).await;
// Yield the assistant's response
yield response.clone();
tokio::task::yield_now().await;
// First collect any tool requests
let tool_requests: Vec<&ToolRequest> = response.content
.iter()
.filter_map(|content| content.as_tool_request())
.collect();
if tool_requests.is_empty() {
break;
}
// Then dispatch each in parallel
let futures: Vec<_> = tool_requests
.iter()
.filter_map(|request| request.tool_call.clone().ok())
.map(|tool_call| capabilities.dispatch_tool_call(tool_call))
.collect();
// Process all the futures in parallel but wait until all are finished
let outputs = futures::future::join_all(futures).await;
// Create a message with the responses
let mut message_tool_response = Message::user();
// Now combine these into MessageContent::ToolResponse using the original ID
for (request, output) in tool_requests.iter().zip(outputs.into_iter()) {
message_tool_response = message_tool_response.with_tool_response(
request.id.clone(),
output,
);
}
yield message_tool_response.clone();
messages.push(response);
messages.push(message_tool_response);
}
}))
}
async fn usage(&self) -> Vec<ProviderUsage> {
let capabilities = self.capabilities.lock().await;
capabilities.get_usage().await
}
}
register_agent!("reference", ReferenceAgent);
+4
View File
@@ -107,6 +107,10 @@ impl From<Content> for MessageContent {
match content {
Content::Text(text) => MessageContent::Text(text),
Content::Image(image) => MessageContent::Image(image),
Content::Resource(resource) => MessageContent::Text(TextContent {
text: resource.get_text(),
annotations: None,
}),
}
}
}
@@ -89,6 +89,9 @@ pub fn messages_to_openai_spec(
"content": [convert_image(&image, image_format)]
}));
}
Content::Resource(resource) => {
tool_content.push(Content::text(resource.get_text()));
}
_ => {
tool_content.push(content);
}
+45
View File
@@ -2,6 +2,7 @@
/// The various content types can be display to humans but also understood by models
/// They include optional annotations used to help inform agent usage
use super::role::Role;
use crate::resource::ResourceContents;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@@ -49,11 +50,29 @@ pub struct ImageContent {
pub annotations: Option<Annotations>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmbeddedResource {
resource: ResourceContents,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
impl EmbeddedResource {
pub fn get_text(&self) -> String {
match &self.resource {
ResourceContents::TextResourceContents { text, .. } => text.clone(),
_ => String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Content {
Text(TextContent),
Image(ImageContent),
Resource(EmbeddedResource),
}
impl Content {
@@ -72,6 +91,24 @@ impl Content {
})
}
pub fn resource(resource: ResourceContents) -> Self {
Content::Resource(EmbeddedResource {
resource,
annotations: None,
})
}
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
Content::Resource(EmbeddedResource {
resource: ResourceContents::TextResourceContents {
uri: uri.into(),
mime_type: Some("text".to_string()),
text: content.into(),
},
annotations: None,
})
}
/// Get the text content if this is a TextContent variant
pub fn as_text(&self) -> Option<&str> {
match self {
@@ -93,6 +130,7 @@ impl Content {
let annotations = match &mut self {
Content::Text(text) => &mut text.annotations,
Content::Image(image) => &mut image.annotations,
Content::Resource(resource) => &mut resource.annotations,
};
*annotations = Some(match annotations.take() {
Some(mut a) => {
@@ -118,6 +156,7 @@ impl Content {
let annotations = match &mut self {
Content::Text(text) => &mut text.annotations,
Content::Image(image) => &mut image.annotations,
Content::Resource(resource) => &mut resource.annotations,
};
*annotations = Some(match annotations.take() {
Some(mut a) => {
@@ -138,6 +177,10 @@ impl Content {
match self {
Content::Text(text) => text.annotations.as_ref().and_then(|a| a.audience.as_ref()),
Content::Image(image) => image.annotations.as_ref().and_then(|a| a.audience.as_ref()),
Content::Resource(resource) => resource
.annotations
.as_ref()
.and_then(|a| a.audience.as_ref()),
}
}
@@ -146,6 +189,7 @@ impl Content {
match self {
Content::Text(text) => text.annotations.as_ref().and_then(|a| a.priority),
Content::Image(image) => image.annotations.as_ref().and_then(|a| a.priority),
Content::Resource(resource) => resource.annotations.as_ref().and_then(|a| a.priority),
}
}
@@ -153,6 +197,7 @@ impl Content {
match self {
Content::Text(text) => Content::text(text.text.clone()),
Content::Image(image) => Content::image(image.data.clone(), image.mime_type.clone()),
Content::Resource(resource) => Content::resource(resource.resource.clone()),
}
}
}