Skip to content
Draft
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
39 changes: 37 additions & 2 deletions crates/forge_app/src/dto/google/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ impl From<Context> for Request {
contents.push(Content { role: Some(Role::User), parts: pending_tool_parts });
}

let conversation_id = context.conversation_id.map(|id| id.to_string());

// Convert tools
let tools = if !context.tools.is_empty() {
Some(vec![Tool::FunctionDeclarations {
Expand Down Expand Up @@ -424,7 +426,11 @@ impl From<Context> for Request {
tool_config,
safety_settings: None,
cached_content: None,
labels: None,
labels: conversation_id.map(|conversation_id| {
serde_json::json!({
"conversation_id": conversation_id,
})
}),
}
}
}
Expand Down Expand Up @@ -561,10 +567,39 @@ impl From<forge_domain::Image> for Part {

#[cfg(test)]
mod tests {
use forge_domain::{ToolCallArguments, ToolCallFull, ToolCallId, ToolName, ToolResult};
use forge_domain::{
Context, ConversationId, ToolCallArguments, ToolCallFull, ToolCallId, ToolName, ToolResult,
};
use pretty_assertions::assert_eq;

use super::*;

#[test]
fn test_request_includes_conversation_id_label_when_present() {
let fixture = ConversationId::generate();
let actual =
serde_json::to_value(Request::from(Context::default().conversation_id(fixture)))
.unwrap();
let expected = serde_json::Value::String(fixture.to_string());

assert_eq!(actual["labels"]["conversation_id"], expected);
}

#[test]
fn test_request_omits_conversation_id_label_when_absent() {
let fixture = Context::default();
let actual = serde_json::to_value(Request::from(fixture)).unwrap();
let expected = serde_json::Value::Null;

assert_eq!(
actual
.get("labels")
.cloned()
.unwrap_or(serde_json::Value::Null),
expected
);
}

#[test]
fn test_tool_call_args_serialization() {
// Create a ToolCallFull with Unparsed JSON arguments (as it would come from
Expand Down
4 changes: 4 additions & 0 deletions crates/forge_app/src/infra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ pub trait FileWriterInfra: Send + Sync {
/// Writes the content of a file at the specified path.
async fn write(&self, path: &Path, contents: Bytes) -> anyhow::Result<()>;

/// Appends content to a file at the specified path, creating the file if it
/// does not already exist.
async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()>;

/// Writes content to a temporary file with the given prefix and extension,
/// and returns its path. The file will be kept (not deleted) after
/// creation.
Expand Down
16 changes: 16 additions & 0 deletions crates/forge_fs/src/write.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::Path;

use anyhow::{Context, Result};
use tokio::io::AsyncWriteExt;

impl crate::ForgeFS {
pub async fn create_dir_all<T: AsRef<Path>>(path: T) -> Result<()> {
Expand All @@ -15,6 +16,21 @@ impl crate::ForgeFS {
.with_context(|| format!("Failed to write file {}", path.as_ref().display()))
}

pub async fn append<T: AsRef<Path>, U: AsRef<[u8]>>(path: T, contents: U) -> Result<()> {
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path.as_ref())
.await
.with_context(|| {
format!("Failed to open file {} for append", path.as_ref().display())
})?;

file.write_all(contents.as_ref())
.await
.with_context(|| format!("Failed to append file {}", path.as_ref().display()))
}

pub async fn remove_file<T: AsRef<Path>>(path: T) -> Result<()> {
tokio::fs::remove_file(path.as_ref())
.await
Expand Down
4 changes: 4 additions & 0 deletions crates/forge_infra/src/forge_infra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ impl FileWriterInfra for ForgeInfra {
self.file_write_service.write(path, contents).await
}

async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> {
self.file_write_service.append(path, contents).await
}

async fn write_temp(&self, prefix: &str, ext: &str, content: &str) -> anyhow::Result<PathBuf> {
self.file_write_service
.write_temp(prefix, ext, content)
Expand Down
5 changes: 5 additions & 0 deletions crates/forge_infra/src/fs_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ impl FileWriterInfra for ForgeFileWriteService {
Ok(forge_fs::ForgeFS::write(path, contents.to_vec()).await?)
}

async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> {
self.create_parent_dirs(path).await?;
Ok(forge_fs::ForgeFS::append(path, contents.to_vec()).await?)
}

async fn write_temp(&self, prefix: &str, ext: &str, content: &str) -> anyhow::Result<PathBuf> {
let path = tempfile::Builder::new()
.disable_cleanup(true)
Expand Down
Loading
Loading