| use std::sync::Arc; |
|
|
| use anyhow::{Context, Result}; |
| use async_trait::async_trait; |
| use chrono::Utc; |
| use forge_app::GrpcInfra; |
| use forge_domain::{ |
| ApiKey, FileUploadInfo, Node, UserId, WorkspaceAuth, WorkspaceId, WorkspaceIndexRepository, |
| WorkspaceInfo, |
| }; |
|
|
| use crate::proto_generated::forge_service_client::ForgeServiceClient; |
| use crate::proto_generated::{self, *}; |
|
|
| |
|
|
| impl TryFrom<CreateApiKeyResponse> for WorkspaceAuth { |
| type Error = anyhow::Error; |
|
|
| fn try_from(response: CreateApiKeyResponse) -> Result<Self> { |
| let user_id = response.user_id.context("Missing user_id in response")?.id; |
| let user_id = UserId::from_string(&user_id).context("Invalid user_id returned from API")?; |
| let token: ApiKey = response.key.into(); |
|
|
| Ok(WorkspaceAuth { user_id, token, created_at: Utc::now() }) |
| } |
| } |
|
|
| impl TryFrom<CreateWorkspaceResponse> for WorkspaceId { |
| type Error = anyhow::Error; |
|
|
| fn try_from(response: CreateWorkspaceResponse) -> Result<Self> { |
| let workspace = response.workspace.context("No workspace in response")?; |
| let workspace_id = workspace |
| .workspace_id |
| .context("Server did not return workspace ID in CreateWorkspace response")? |
| .id; |
|
|
| WorkspaceId::from_string(&workspace_id) |
| .context("Failed to parse workspace ID from server response") |
| } |
| } |
|
|
| impl TryFrom<Workspace> for WorkspaceInfo { |
| type Error = anyhow::Error; |
|
|
| fn try_from(workspace: Workspace) -> Result<Self> { |
| let id_msg = workspace |
| .workspace_id |
| .context("Missing workspace_id in response")?; |
| let workspace_id = |
| WorkspaceId::from_string(&id_msg.id).context("Failed to parse workspace ID")?; |
|
|
| let last_updated = workspace |
| .last_updated |
| .and_then(|ts| chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32)); |
|
|
| let created_at = workspace |
| .created_at |
| .and_then(|ts| chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32)) |
| .context("Missing or invalid created_at")?; |
|
|
| Ok(WorkspaceInfo { |
| workspace_id, |
| working_dir: workspace.working_dir, |
| node_count: workspace.node_count, |
| relation_count: workspace.relation_count, |
| last_updated, |
| created_at, |
| }) |
| } |
| } |
|
|
| impl TryFrom<FileRefNode> for forge_domain::FileHash { |
| type Error = anyhow::Error; |
|
|
| fn try_from(file_ref_node: FileRefNode) -> Result<Self> { |
| let data = file_ref_node.data.context("Missing data in FileRefNode")?; |
| Ok(forge_domain::FileHash { path: data.path, hash: data.file_hash }) |
| } |
| } |
|
|
| |
| |
| |
| pub struct ForgeContextEngineRepository<I> { |
| infra: Arc<I>, |
| } |
|
|
| impl<I> ForgeContextEngineRepository<I> { |
| |
| |
| |
| |
| pub fn new(infra: Arc<I>) -> Self { |
| Self { infra } |
| } |
|
|
| |
| |
| |
| |
| fn with_auth<T>( |
| &self, |
| mut request: tonic::Request<T>, |
| auth_token: &ApiKey, |
| ) -> Result<tonic::Request<T>> { |
| request |
| .metadata_mut() |
| .insert("authorization", format!("Bearer {}", **auth_token).parse()?); |
| Ok(request) |
| } |
| } |
|
|
| #[async_trait] |
| impl<I: GrpcInfra> WorkspaceIndexRepository for ForgeContextEngineRepository<I> { |
| async fn authenticate(&self) -> Result<WorkspaceAuth> { |
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let request = tonic::Request::new(CreateApiKeyRequest { user_id: None }); |
|
|
| let response = client |
| .create_api_key(request) |
| .await |
| .context("Failed to call CreateApiKey gRPC")? |
| .into_inner(); |
|
|
| response.try_into() |
| } |
|
|
| async fn create_workspace( |
| &self, |
| working_dir: &std::path::Path, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<WorkspaceId> { |
| let request = tonic::Request::new(CreateWorkspaceRequest { |
| workspace: Some(WorkspaceDefinition { |
| working_dir: working_dir.to_string_lossy().replace("\\", "/"), |
| ..Default::default() |
| }), |
| }); |
|
|
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let response = client.create_workspace(request).await?.into_inner(); |
|
|
| response.try_into() |
| } |
|
|
| async fn upload_files( |
| &self, |
| upload: &forge_domain::FileUpload, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<FileUploadInfo> { |
| let files: Vec<File> = upload |
| .data |
| .iter() |
| .map(|file_read| File { |
| path: file_read.path.clone(), |
| content: file_read.content.clone(), |
| }) |
| .collect(); |
|
|
| let request = tonic::Request::new(UploadFilesRequest { |
| workspace_id: Some(proto_generated::WorkspaceId { |
| id: upload.workspace_id.to_string(), |
| }), |
| content: Some(FileUploadContent { files, git: None }), |
| }); |
|
|
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let response = client.upload_files(request).await?; |
|
|
| let result = response |
| .into_inner() |
| .result |
| .context("Server did not return upload result in UploadFiles response")?; |
|
|
| Ok(FileUploadInfo::new( |
| result.node_ids.len(), |
| result.relations.len(), |
| )) |
| } |
|
|
| |
| async fn search( |
| &self, |
| search_query: &forge_domain::CodeSearchQuery<'_>, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<Vec<Node>> { |
| let request = tonic::Request::new(SearchRequest { |
| workspace_id: Some(proto_generated::WorkspaceId { |
| id: search_query.workspace_id.to_string(), |
| }), |
| query: Some(Query { |
| prompt: Some(search_query.data.query.to_string()), |
| limit: search_query.data.limit.map(|l| l as u32), |
| top_k: search_query.data.top_k, |
| relevance_query: Some(search_query.data.use_case.to_string()), |
| starts_with: search_query.data.starts_with.clone().into_iter().collect(), |
| ends_with: search_query.data.ends_with.clone().unwrap_or_default(), |
| max_distance: None, |
| kinds: vec![NodeKind::FileChunk.into()], |
| }), |
| }); |
|
|
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let response = client.search(request).await?; |
|
|
| let result = response.into_inner().result.unwrap_or_default(); |
|
|
| |
| let results = result |
| .data |
| .into_iter() |
| .filter_map(|query_item| { |
| let node = query_item.node?; |
| let node_data = node.data?; |
| let node_id = node.node_id.map(|n| n.id).unwrap_or_default(); |
|
|
| |
| let relevance = query_item.relevance; |
| let distance = query_item.distance; |
|
|
| |
| let code_node = match node_data.kind? { |
| node_data::Kind::FileChunk(chunk) => { |
| forge_domain::NodeData::FileChunk(forge_domain::FileChunk { |
| file_path: chunk.path, |
| content: chunk.content, |
| start_line: chunk.start_line, |
| end_line: chunk.end_line, |
| }) |
| } |
| node_data::Kind::File(file) => { |
| forge_domain::NodeData::File(forge_domain::FileNode { |
| file_path: file.path, |
| content: file.content, |
| hash: node.hash, |
| }) |
| } |
| node_data::Kind::FileRef(file_ref) => { |
| forge_domain::NodeData::FileRef(forge_domain::FileRef { |
| file_path: file_ref.path, |
| file_hash: file_ref.file_hash, |
| }) |
| } |
| node_data::Kind::Note(note) => { |
| forge_domain::NodeData::Note(forge_domain::Note { content: note.content }) |
| } |
| node_data::Kind::Task(task) => { |
| forge_domain::NodeData::Task(forge_domain::Task { task: task.task }) |
| } |
| }; |
|
|
| |
| Some(Node { |
| node_id: node_id.into(), |
| node: code_node, |
| relevance, |
| distance, |
| }) |
| }) |
| .collect(); |
|
|
| Ok(results) |
| } |
|
|
| |
| async fn list_workspaces( |
| &self, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<Vec<WorkspaceInfo>> { |
| let request = tonic::Request::new(ListWorkspacesRequest {}); |
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let response = client.list_workspaces(request).await?; |
|
|
| response |
| .into_inner() |
| .workspaces |
| .into_iter() |
| .map(|workspace| workspace.try_into()) |
| .collect() |
| } |
|
|
| |
| async fn get_workspace( |
| &self, |
| workspace_id: &WorkspaceId, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<Option<WorkspaceInfo>> { |
| let request = tonic::Request::new(GetWorkspaceInfoRequest { |
| workspace_id: Some(proto_generated::WorkspaceId { id: workspace_id.to_string() }), |
| }); |
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let response = client.get_workspace_info(request).await?; |
|
|
| let workspace = response.into_inner().workspace; |
| workspace.map(|w| w.try_into()).transpose() |
| } |
|
|
| |
| async fn list_workspace_files( |
| &self, |
| workspace: &forge_domain::WorkspaceFiles, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<Vec<forge_domain::FileHash>> { |
| let request = tonic::Request::new(ListFilesRequest { |
| workspace_id: Some(proto_generated::WorkspaceId { |
| id: workspace.workspace_id.to_string(), |
| }), |
| }); |
|
|
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| let response = client.list_files(request).await?; |
|
|
| response |
| .into_inner() |
| .files |
| .into_iter() |
| .map(|file_ref_node| file_ref_node.try_into()) |
| .collect() |
| } |
|
|
| |
| async fn delete_files( |
| &self, |
| deletion: &forge_domain::FileDeletion, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<()> { |
| if deletion.data.is_empty() { |
| return Ok(()); |
| } |
|
|
| let request = tonic::Request::new(DeleteFilesRequest { |
| workspace_id: Some(proto_generated::WorkspaceId { |
| id: deletion.workspace_id.to_string(), |
| }), |
| file_paths: deletion.data.clone(), |
| }); |
|
|
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| client.delete_files(request).await?; |
|
|
| Ok(()) |
| } |
|
|
| async fn delete_workspace( |
| &self, |
| workspace_id: &forge_domain::WorkspaceId, |
| auth_token: &forge_domain::ApiKey, |
| ) -> Result<()> { |
| let request = tonic::Request::new(DeleteWorkspaceRequest { |
| workspace_id: Some(proto_generated::WorkspaceId { id: workspace_id.to_string() }), |
| }); |
|
|
| let request = self.with_auth(request, auth_token)?; |
|
|
| let channel = self.infra.channel()?; |
| let mut client = ForgeServiceClient::new(channel); |
| client.delete_workspace(request).await?; |
|
|
| Ok(()) |
| } |
| } |
|
|