diff --git a/libs/fctool/src/project_tools/repos.rs b/libs/fctool/src/project_tools/repos.rs index 0b39e60..0f104c8 100644 --- a/libs/fctool/src/project_tools/repos.rs +++ b/libs/fctool/src/project_tools/repos.rs @@ -169,6 +169,19 @@ pub async fn create_repo_exec( git2::Repository::init_bare(&repo_dir) .map_err(|e| ToolError::ExecutionError(format!("Failed to init bare repo: {}", e)))?; + // Embed repo into Qdrant for semantic search (non-blocking) + if let Some(embed) = ctx.embed_service() { + let es = embed.clone(); + let repo_id = model.id.to_string(); + let repo_name = model.repo_name.clone(); + let repo_desc = model.description.clone(); + tokio::spawn(async move { + if let Err(e) = es.embed_repo(&repo_id, &repo_name, repo_desc.as_deref()).await { + tracing::warn!(error = %e, repo_id = %repo_id, "failed to embed repo"); + } + }); + } + Ok(serde_json::json!({ "id": model.id.to_string(), "name": model.repo_name, @@ -256,6 +269,19 @@ pub async fn update_repo_exec( .await .map_err(|e| ToolError::ExecutionError(e.to_string()))?; + // Re-embed repo on update (non-blocking) + if let Some(embed) = ctx.embed_service() { + let es = embed.clone(); + let repo_id = model.id.to_string(); + let repo_name = model.repo_name.clone(); + let repo_desc = model.description.clone(); + tokio::spawn(async move { + if let Err(e) = es.embed_repo(&repo_id, &repo_name, repo_desc.as_deref()).await { + tracing::warn!(error = %e, repo_id = %repo_id, "failed to re-embed repo on update"); + } + }); + } + Ok(serde_json::json!({ "id": model.id.to_string(), "name": model.repo_name, diff --git a/libs/service/issue/issue.rs b/libs/service/issue/issue.rs index 6959581..9b84d55 100644 --- a/libs/service/issue/issue.rs +++ b/libs/service/issue/issue.rs @@ -292,6 +292,19 @@ impl AppService { let _ = this.triage_issue(project_name_clone, issue_number).await; }); + // Embed issue into Qdrant for semantic search (non-blocking) + if let Some(ref embed) = self.embed_service { + let issue_id = model.id.to_string(); + let issue_title = model.title.clone(); + let issue_body = model.body.clone(); + let es = embed.clone(); + tokio::spawn(async move { + if let Err(e) = es.embed_issue_chunked(&issue_id, &issue_title, issue_body.as_deref()).await { + tracing::warn!(error = %e, issue_id = %issue_id, "failed to embed issue"); + } + }); + } + Ok(IssueResponse::from(model)) } diff --git a/libs/service/lib.rs b/libs/service/lib.rs index cceecff..a573320 100644 --- a/libs/service/lib.rs +++ b/libs/service/lib.rs @@ -37,6 +37,7 @@ pub struct AppService { pub queue_producer: MessageProducer, pub storage: Option, pub push: Option, + pub embed_service: Option>, } impl AppService { @@ -185,6 +186,8 @@ impl AppService { } }; + let embed_service_for_app = embed_service.clone(); + // Build ChatService if AI is configured; otherwise AI chat is disabled (graceful degradation) let chat_service: Option> = match (config.ai_api_key(), config.ai_basic_url()) { @@ -276,6 +279,7 @@ impl AppService { queue_producer: message_producer, storage, push, + embed_service: embed_service_for_app, }) } diff --git a/libs/service/skill/manage.rs b/libs/service/skill/manage.rs index 079be2c..ef58bb4 100644 --- a/libs/service/skill/manage.rs +++ b/libs/service/skill/manage.rs @@ -104,6 +104,22 @@ impl AppService { }; let inserted = active.insert(&self.db).await?; + + // Embed skill into Qdrant (non-blocking) + if let Some(ref embed) = self.embed_service { + let es = embed.clone(); + let sid = inserted.id; + let sname = inserted.name.clone(); + let sdesc = inserted.description.clone(); + let scontent = inserted.content.clone(); + let sproj = inserted.project_uuid.to_string(); + tokio::spawn(async move { + if let Err(e) = es.embed_skill(sid, &sname, sdesc.as_deref(), &scontent, &sproj).await { + tracing::warn!(error = %e, skill_id = %sid, "failed to embed skill"); + } + }); + } + Ok(SkillResponse::from(inserted)) } @@ -144,6 +160,22 @@ impl AppService { active.updated_at = Set(Utc::now()); let updated = active.update(&self.db).await?; + + // Re-embed skill on update (non-blocking) + if let Some(ref embed) = self.embed_service { + let es = embed.clone(); + let sid = updated.id; + let sname = updated.name.clone(); + let sdesc = updated.description.clone(); + let scontent = updated.content.clone(); + let sproj = updated.project_uuid.to_string(); + tokio::spawn(async move { + if let Err(e) = es.embed_skill(sid, &sname, sdesc.as_deref(), &scontent, &sproj).await { + tracing::warn!(error = %e, skill_id = %sid, "failed to re-embed skill on update"); + } + }); + } + Ok(SkillResponse::from(updated)) }