diff --git a/Cargo.toml b/Cargo.toml index b508edef5..15fe0ca3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,7 @@ members = [ resolver = "2" [profile.release] -lto = "thin" \ No newline at end of file +lto = "thin" + +[dependencies] +octocrab = "0.32.0" \ No newline at end of file diff --git a/sidecar/src/agentic/tool/git/edited_files.rs b/sidecar/src/agentic/tool/git/edited_files.rs index 3ca5de363..1940165ed 100644 --- a/sidecar/src/agentic/tool/git/edited_files.rs +++ b/sidecar/src/agentic/tool/git/edited_files.rs @@ -32,6 +32,10 @@ pub struct EditedGitDiffFile { diff: String, current_content: String, updated_timestamp_ms: i64, + change_frequency: u32, + importance_score: u8, + semantic_relation_score: f32, + user_interaction_score: u8, } impl EditedGitDiffFile { @@ -50,6 +54,22 @@ impl EditedGitDiffFile { pub fn current_content(&self) -> &str { &self.current_content } + + pub fn change_frequency(&self) -> u32 { + self.change_frequency + } + + pub fn importance_score(&self) -> u8 { + self.importance_score + } + + pub fn semantic_relation_score(&self) -> f32 { + self.semantic_relation_score + } + + pub fn user_interaction_score(&self) -> u8 { + self.user_interaction_score + } } #[derive(Debug, Clone, serde::Deserialize)] @@ -78,20 +98,48 @@ impl EditedFiles { #[async_trait] impl Tool for EditedFiles { async fn invoke(&self, input: ToolInput) -> Result { - let context = input.should_edited_files()?; - let editor_endpoint = context.editor_url.to_owned() + "/recent_edits"; - let response = self - .client - .post(editor_endpoint) - .body(serde_json::to_string(&context).map_err(|_e| ToolError::SerdeConversionFailed)?) - .send() - .await - .map_err(|_e| ToolError::ErrorCommunicatingWithEditor)?; - let response: EditedFilesResponse = response.json().await.map_err(|e| { - eprintln!("edited_files::{:?}", &e); - ToolError::SerdeConversionFailed - })?; - Ok(ToolOutput::edited_files(response)) + match input { + ToolInput::EditedFiles(request) => { + let response = self + .client + .get(&format!("{}/api/v1/edited-files", request.editor_url)) + .send() + .await + .map_err(|e| ToolError::NetworkError(e.to_string()))? + .json::() + .await + .map_err(|e| ToolError::DeserializationError(e.to_string()))?; + + // Calculate weights for each file + let weighted_files = response.changed_files().into_iter().map(|file| { + let weight = ChangeWeight::new( + file.change_frequency(), + file.importance_score(), + file.semantic_relation_score(), + file.user_interaction_score(), + ); + (file, weight) + }); + + // Create DiffFileContent with weights + let file_contents = weighted_files + .map(|(file, weight)| { + DiffFileContent::new( + file.fs_file_path().to_owned(), + file.current_content().to_owned(), + None, + Some(weight), + file.updated_timestamp_ms(), + ) + }) + .collect(); + + Ok(ToolOutput::EditedFiles(EditedFilesResponse { + changed_files: file_contents, + })) + } + _ => Err(ToolError::WrongToolInput), + } } fn tool_description(&self) -> String { @@ -106,7 +154,7 @@ impl Tool for EditedFiles { vec![] } - fn get_reward_scale(&self, _trajectory_length: usize) -> Vec { - vec![] + fn reward_scale(&self) -> ToolRewardScale { + ToolRewardScale::new(1.0, 0.0) } } diff --git a/sidecar/src/agentic/tool/helpers/diff_recent_changes.rs b/sidecar/src/agentic/tool/helpers/diff_recent_changes.rs index 92a62a18e..b03280453 100644 --- a/sidecar/src/agentic/tool/helpers/diff_recent_changes.rs +++ b/sidecar/src/agentic/tool/helpers/diff_recent_changes.rs @@ -5,12 +5,45 @@ use llm_client::clients::types::LLMClientMessage; +#[derive(Debug, Clone, serde::Serialize)] +pub struct ChangeWeight { + frequency: u32, + importance: u8, + semantic_relation: f32, + user_interaction: u8, +} + +impl ChangeWeight { + pub fn new( + frequency: u32, + importance: u8, + semantic_relation: f32, + user_interaction: u8, + ) -> Self { + Self { + frequency, + importance, + semantic_relation, + user_interaction, + } + } + + pub fn calculate_score(&self) -> f32 { + (self.frequency as f32 * 0.3) + + (self.importance as f32 * 0.3) + + (self.semantic_relation * 0.2) + + (self.user_interaction as f32 * 0.2) + } +} + #[derive(Debug, Clone, serde::Serialize)] pub struct DiffFileContent { fs_file_path: String, file_content_latest: String, - // we can set this if we already have the file content file_content_updated: Option, + change_weight: Option, + last_modified: i64, + is_invalidated: bool, } impl DiffFileContent { @@ -18,14 +51,31 @@ impl DiffFileContent { fs_file_path: String, file_content_latest: String, file_content_updated: Option, + change_weight: Option, + last_modified: i64, ) -> Self { Self { fs_file_path, file_content_latest, file_content_updated, + change_weight, + last_modified, + is_invalidated: false, } } + pub fn invalidate(&mut self) { + self.is_invalidated = true; + } + + pub fn is_invalidated(&self) -> bool { + self.is_invalidated + } + + pub fn update_weight(&mut self, weight: ChangeWeight) { + self.change_weight = Some(weight); + } + pub fn fs_file_path(&self) -> &str { &self.fs_file_path } @@ -42,6 +92,7 @@ pub struct DiffRecentChanges { l1_changes: String, l2_changes: String, file_contents: Vec, + cache_threshold: f32, } impl DiffRecentChanges { @@ -49,11 +100,13 @@ impl DiffRecentChanges { l1_changes: String, l2_changes: String, file_contents: Vec, + cache_threshold: f32, ) -> Self { Self { l1_changes, l2_changes, file_contents, + cache_threshold, } } @@ -69,6 +122,24 @@ impl DiffRecentChanges { &self.l2_changes } + pub fn should_promote_to_l1(&self, file_content: &DiffFileContent) -> bool { + if let Some(weight) = &file_content.change_weight { + weight.calculate_score() >= self.cache_threshold + } else { + false + } + } + + pub fn invalidate_cache_for_file(&mut self, file_path: &str) { + if let Some(file_content) = self + .file_contents + .iter_mut() + .find(|f| f.fs_file_path() == file_path) + { + file_content.invalidate(); + } + } + pub fn to_llm_client_message(&self) -> Vec { let l1_changes = self.l1_changes(); let l2_changes = self.l2_changes(); diff --git a/sidecar/src/agentic/tool/helpers/diff_recent_changes_test.rs b/sidecar/src/agentic/tool/helpers/diff_recent_changes_test.rs new file mode 100644 index 000000000..9c0584048 --- /dev/null +++ b/sidecar/src/agentic/tool/helpers/diff_recent_changes_test.rs @@ -0,0 +1,64 @@ +use super::diff_recent_changes::{ChangeWeight, DiffFileContent, DiffRecentChanges}; + +#[test] +fn test_change_weight_calculation() { + let weight = ChangeWeight::new(10, 8, 0.75, 7); + let score = weight.calculate_score(); + assert!(score > 0.0); + assert!(score <= 10.0); +} + +#[test] +fn test_cache_promotion() { + let file_content = DiffFileContent::new( + "test.rs".to_string(), + "content".to_string(), + None, + Some(ChangeWeight::new(10, 8, 0.75, 7)), + 1234567890, + ); + + let changes = DiffRecentChanges::new( + "".to_string(), + "".to_string(), + vec![file_content.clone()], + 5.0, + ); + + assert!(changes.should_promote_to_l1(&file_content)); +} + +#[test] +fn test_cache_invalidation() { + let mut file_content = DiffFileContent::new( + "test.rs".to_string(), + "content".to_string(), + None, + Some(ChangeWeight::new(10, 8, 0.75, 7)), + 1234567890, + ); + + assert!(!file_content.is_invalidated()); + file_content.invalidate(); + assert!(file_content.is_invalidated()); +} + +#[test] +fn test_weight_update() { + let mut file_content = DiffFileContent::new( + "test.rs".to_string(), + "content".to_string(), + None, + None, + 1234567890, + ); + + let new_weight = ChangeWeight::new(5, 6, 0.5, 4); + file_content.update_weight(new_weight.clone()); + + assert!(file_content.change_weight.is_some()); + assert_eq!( + file_content.change_weight.unwrap().calculate_score(), + new_weight.calculate_score() + ); +} diff --git a/sidecar/src/agentic/tool/swe_bench/test_tool.rs b/sidecar/src/agentic/tool/swe_bench/test_tool.rs index 81b85c70a..a6e0a6474 100644 --- a/sidecar/src/agentic/tool/swe_bench/test_tool.rs +++ b/sidecar/src/agentic/tool/swe_bench/test_tool.rs @@ -8,27 +8,46 @@ use crate::agentic::tool::{ }; use async_trait::async_trait; use logging::new_client; +use std::time::Duration; +use tracing::{error, info, warn}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SWEBenchTestRequest { swe_bench_test_endpoint: String, + timeout: Option, + retry_count: Option, } impl SWEBenchTestRequest { pub fn new(swe_bench_test_endpoint: String) -> Self { Self { swe_bench_test_endpoint, + timeout: Some(Duration::from_secs(30)), + retry_count: Some(3), } } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + pub fn with_retry_count(mut self, retry_count: u32) -> Self { + self.retry_count = Some(retry_count); + self + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct SWEBenchTestRepsonse { +pub struct SWEBenchTestResponse { test_output: Option, passed: bool, + execution_time: Option, + error_message: Option, + test_name: Option, } -impl SWEBenchTestRepsonse { +impl SWEBenchTestResponse { pub fn passed(&self) -> bool { self.passed } @@ -36,17 +55,101 @@ impl SWEBenchTestRepsonse { pub fn test_output(&self) -> Option { self.test_output.clone() } + + pub fn execution_time(&self) -> Option { + self.execution_time + } + + pub fn error_message(&self) -> Option { + self.error_message.clone() + } + + pub fn test_name(&self) -> Option { + self.test_name.clone() + } } pub struct SWEBenchTestTool { client: reqwest_middleware::ClientWithMiddleware, + default_timeout: Duration, + default_retry_count: u32, } impl SWEBenchTestTool { pub fn new() -> Self { Self { client: new_client(), + default_timeout: Duration::from_secs(30), + default_retry_count: 3, + } + } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.default_timeout = timeout; + self + } + + pub fn with_retry_count(mut self, retry_count: u32) -> Self { + self.default_retry_count = retry_count; + self + } + + async fn execute_test_with_retry( + &self, + endpoint: &str, + request_body: &str, + timeout: Duration, + retry_count: u32, + ) -> Result { + let mut last_error = None; + + for attempt in 0..retry_count { + info!("Executing test attempt {}/{}", attempt + 1, retry_count); + + match self.execute_test(endpoint, request_body, timeout).await { + Ok(response) => return Ok(response), + Err(e) => { + warn!("Test execution attempt {} failed: {:?}", attempt + 1, e); + last_error = Some(e); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } } + + Err(last_error.unwrap_or(ToolError::SWEBenchTestEndpointError)) + } + + async fn execute_test( + &self, + endpoint: &str, + request_body: &str, + timeout: Duration, + ) -> Result { + let start_time = std::time::Instant::now(); + + let response = self + .client + .post(endpoint) + .timeout(timeout) + .body(request_body.to_owned()) + .send() + .await + .map_err(|e| { + error!("Failed to send test request: {:?}", e); + ToolError::SWEBenchTestEndpointError + })?; + + let execution_time = start_time.elapsed(); + + let response: SWEBenchTestResponse = response.json().await.map_err(|e| { + error!("Failed to parse test response: {:?}", e); + ToolError::SerdeConversionFailed + })?; + + Ok(SWEBenchTestResponse { + execution_time: Some(execution_time), + ..response + }) } } @@ -54,33 +157,61 @@ impl SWEBenchTestTool { impl Tool for SWEBenchTestTool { async fn invoke(&self, input: ToolInput) -> Result { let context = input.swe_bench_test()?; + let timeout = context.timeout.unwrap_or(self.default_timeout); + let retry_count = context.retry_count.unwrap_or(self.default_retry_count); + + info!( + "Executing test with timeout {:?} and {} retries", + timeout, retry_count + ); + let response = self - .client - .post(context.swe_bench_test_endpoint.to_owned()) - .body(serde_json::to_string(&context).map_err(|_e| ToolError::SerdeConversionFailed)?) - .send() - .await - .map_err(|_e| ToolError::SWEBenchTestEndpointError)?; - let response: SWEBenchTestRepsonse = response - .json() - .await - .map_err(|_e| ToolError::SerdeConversionFailed)?; + .execute_test_with_retry( + &context.swe_bench_test_endpoint, + &serde_json::to_string(&context).map_err(|e| { + error!("Failed to serialize test request: {:?}", e); + ToolError::SerdeConversionFailed + })?, + timeout, + retry_count, + ) + .await?; + + info!( + "Test execution completed. Passed: {}, Execution time: {:?}", + response.passed, response.execution_time + ); + Ok(ToolOutput::swe_bench_test_output(response)) } fn tool_description(&self) -> String { - "".to_owned() + "A tool for executing and analyzing SWE-bench tests with retry capabilities and detailed reporting".to_owned() } fn tool_input_format(&self) -> String { - "".to_owned() + r#"{ + "swe_bench_test_endpoint": "string", + "timeout": "optional duration in seconds", + "retry_count": "optional number of retries" + }"# + .to_owned() } fn get_evaluation_criteria(&self, _trajectory_length: usize) -> Vec { - vec![] + vec![ + "Test execution success".to_owned(), + "Execution time within limits".to_owned(), + "Proper error handling".to_owned(), + "Detailed test output".to_owned(), + ] } fn get_reward_scale(&self, _trajectory_length: usize) -> Vec { - vec![] + vec![ + ToolRewardScale::new("test_success", 1.0), + ToolRewardScale::new("execution_time", 0.5), + ToolRewardScale::new("error_handling", 0.3), + ] } } diff --git a/src/agentic/tool/broker.rs b/src/agentic/tool/broker.rs new file mode 100644 index 000000000..8d7b26231 --- /dev/null +++ b/src/agentic/tool/broker.rs @@ -0,0 +1,26 @@ +use crate::agentic::tool::git::tools::{ + GitCommitTool, GitCreateBranchTool, GitPushTool, GitCreatePullRequestTool +}; + +impl ToolBroker { + pub async fn new( + llm_client: Arc, + code_edit_broker: Arc, + symbol_tracking: Arc, + language_broker: Arc, + tool_broker_config: ToolBrokerConfiguration, + fail_over_llm: LLMProperties, + ) -> Self { + let mut tools = HashMap::new(); + + // ... existing tool registrations ... + + // Register git tools + tools.insert(ToolType::GitCommit, Box::new(GitCommitTool) as Box); + tools.insert(ToolType::GitCreateBranch, Box::new(GitCreateBranchTool) as Box); + tools.insert(ToolType::GitPush, Box::new(GitPushTool) as Box); + tools.insert(ToolType::GitCreatePullRequest, Box::new(GitCreatePullRequestTool) as Box); + + // ... rest of the implementation ... + } +} \ No newline at end of file diff --git a/src/agentic/tool/git/tools.rs b/src/agentic/tool/git/tools.rs new file mode 100644 index 000000000..854a10025 --- /dev/null +++ b/src/agentic/tool/git/tools.rs @@ -0,0 +1,148 @@ +use std::path::PathBuf; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::{ + agentic::tool::{ + errors::ToolError, + input::ToolInput, + output::ToolOutput, + r#type::{Tool, ToolRewardScale, ToolType}, + }, + git::operations::{ + commit_changes, create_branch, create_pull_request, push_changes, GitCommitOptions, + GitPullRequestOptions, + }, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitCommitRequest { + pub repo_path: PathBuf, + pub message: String, + pub author_name: String, + pub author_email: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitCreateBranchRequest { + pub repo_path: PathBuf, + pub branch_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitPushRequest { + pub repo_path: PathBuf, + pub branch_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitCreatePullRequestRequest { + pub repo_path: PathBuf, + pub title: String, + pub body: String, + pub base_branch: String, + pub head_branch: String, + pub github_token: String, +} + +pub struct GitCommitTool; +pub struct GitCreateBranchTool; +pub struct GitPushTool; +pub struct GitCreatePullRequestTool; + +#[async_trait] +impl Tool for GitCommitTool { + async fn execute(&self, input: ToolInput) -> Result { + let request: GitCommitRequest = serde_json::from_value(input.input)?; + + let options = GitCommitOptions { + message: request.message, + author_name: request.author_name, + author_email: request.author_email, + }; + + commit_changes(&request.repo_path, &options) + .await + .map_err(|e| ToolError::ExecutionError(e.to_string()))?; + + Ok(ToolOutput::Success("Changes committed successfully".to_string())) + } + + fn tool_type(&self) -> ToolType { + ToolType::GitCommit + } + + fn reward_scale(&self) -> ToolRewardScale { + ToolRewardScale::High + } +} + +#[async_trait] +impl Tool for GitCreateBranchTool { + async fn execute(&self, input: ToolInput) -> Result { + let request: GitCreateBranchRequest = serde_json::from_value(input.input)?; + + create_branch(&request.repo_path, &request.branch_name) + .await + .map_err(|e| ToolError::ExecutionError(e.to_string()))?; + + Ok(ToolOutput::Success(format!("Branch '{}' created successfully", request.branch_name))) + } + + fn tool_type(&self) -> ToolType { + ToolType::GitCreateBranch + } + + fn reward_scale(&self) -> ToolRewardScale { + ToolRewardScale::High + } +} + +#[async_trait] +impl Tool for GitPushTool { + async fn execute(&self, input: ToolInput) -> Result { + let request: GitPushRequest = serde_json::from_value(input.input)?; + + push_changes(&request.repo_path, &request.branch_name) + .await + .map_err(|e| ToolError::ExecutionError(e.to_string()))?; + + Ok(ToolOutput::Success(format!("Changes pushed to branch '{}' successfully", request.branch_name))) + } + + fn tool_type(&self) -> ToolType { + ToolType::GitPush + } + + fn reward_scale(&self) -> ToolRewardScale { + ToolRewardScale::High + } +} + +#[async_trait] +impl Tool for GitCreatePullRequestTool { + async fn execute(&self, input: ToolInput) -> Result { + let request: GitCreatePullRequestRequest = serde_json::from_value(input.input)?; + + let options = GitPullRequestOptions { + title: request.title, + body: request.body, + base_branch: request.base_branch, + head_branch: request.head_branch, + }; + + create_pull_request(&request.repo_path, &request.github_token, &options) + .await + .map_err(|e| ToolError::ExecutionError(e.to_string()))?; + + Ok(ToolOutput::Success("Pull request created successfully".to_string())) + } + + fn tool_type(&self) -> ToolType { + ToolType::GitCreatePullRequest + } + + fn reward_scale(&self) -> ToolRewardScale { + ToolRewardScale::High + } +} \ No newline at end of file diff --git a/src/agentic/tool/r#type/mod.rs b/src/agentic/tool/r#type/mod.rs new file mode 100644 index 000000000..5018bcd4c --- /dev/null +++ b/src/agentic/tool/r#type/mod.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub enum ToolType { + // ... existing tool types ... + GitCommit, + GitCreateBranch, + GitPush, + GitCreatePullRequest, + // ... existing code ... +} \ No newline at end of file diff --git a/src/bin/agent_bin.rs b/src/bin/agent_bin.rs new file mode 100644 index 000000000..b958773ff --- /dev/null +++ b/src/bin/agent_bin.rs @@ -0,0 +1,13 @@ +let tools = vec![ + ToolType::ListFiles, + ToolType::SearchFileContentWithRegex, + ToolType::OpenFile, + ToolType::CodeEditing, + ToolType::AttemptCompletion, + ToolType::TerminalCommand, + ToolType::FindFiles, + ToolType::GitCommit, + ToolType::GitCreateBranch, + ToolType::GitPush, + ToolType::GitCreatePullRequest, +]; \ No newline at end of file diff --git a/src/bin/swe_bench_agent_bin.rs b/src/bin/swe_bench_agent_bin.rs new file mode 100644 index 000000000..b958773ff --- /dev/null +++ b/src/bin/swe_bench_agent_bin.rs @@ -0,0 +1,13 @@ +let tools = vec![ + ToolType::ListFiles, + ToolType::SearchFileContentWithRegex, + ToolType::OpenFile, + ToolType::CodeEditing, + ToolType::AttemptCompletion, + ToolType::TerminalCommand, + ToolType::FindFiles, + ToolType::GitCommit, + ToolType::GitCreateBranch, + ToolType::GitPush, + ToolType::GitCreatePullRequest, +]; \ No newline at end of file diff --git a/src/git/operations.rs b/src/git/operations.rs new file mode 100644 index 000000000..64ba8a697 --- /dev/null +++ b/src/git/operations.rs @@ -0,0 +1,105 @@ +use std::path::Path; +use anyhow::{Context, Result}; +use gix::{self, Repository}; +use octocrab::Octocrab; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct GitCommitOptions { + pub message: String, + pub author_name: String, + pub author_email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GitPullRequestOptions { + pub title: String, + pub body: String, + pub base_branch: String, + pub head_branch: String, +} + +pub async fn commit_changes(repo_path: &Path, options: &GitCommitOptions) -> Result<()> { + let repo = gix::open(repo_path)?; + let mut index = repo.index()?; + + // Stage all changes + index.add_all()?; + + // Create commit + let tree_id = index.write_tree()?; + let tree = repo.find_tree(tree_id)?; + + let head = repo.head()?; + let parent_commit = head.peel_to_commit()?; + + repo.commit( + Some("HEAD"), + &options.author_name, + &options.author_email, + &options.message, + &tree, + &[&parent_commit], + )?; + + Ok(()) +} + +pub async fn create_branch(repo_path: &Path, branch_name: &str) -> Result<()> { + let repo = gix::open(repo_path)?; + let head = repo.head()?; + let commit = head.peel_to_commit()?; + + repo.branch(branch_name, &commit, false)?; + + Ok(()) +} + +pub async fn push_changes(repo_path: &Path, branch_name: &str) -> Result<()> { + let repo = gix::open(repo_path)?; + let mut remote = repo.find_remote("origin")?; + + remote.push(&[format!("refs/heads/{}", branch_name)])?; + + Ok(()) +} + +pub async fn create_pull_request( + repo_path: &Path, + github_token: &str, + options: &GitPullRequestOptions, +) -> Result<()> { + let repo = gix::open(repo_path)?; + let remote_url = repo + .find_remote("origin")? + .url() + .context("Failed to get remote URL")? + .to_string(); + + // Extract owner and repo from remote URL + let (owner, repo_name) = parse_github_url(&remote_url)?; + + // Create GitHub client + let octocrab = Octocrab::builder() + .personal_token(github_token.to_string()) + .build()?; + + // Create pull request + octocrab + .pulls(owner, repo_name) + .create(&options.title, &options.head_branch, &options.base_branch) + .body(&options.body) + .send() + .await?; + + Ok(()) +} + +fn parse_github_url(url: &str) -> Result<(&str, &str)> { + // Extract owner and repo from GitHub URL (e.g., "https://github.com/owner/repo.git") + let parts: Vec<&str> = url.trim_end_matches(".git").split('/').collect(); + let repo = parts.last().context("No repository name found")?; + let owner = parts.get(parts.len() - 2).context("No owner found")?; + + Ok((owner, repo)) +} \ No newline at end of file