feat(service): trigger Qdrant embedding on issue/repo/skill creation

- After issue_create: spawn embed_issue_chunked (non-blocking)
- After skill_create/update: spawn embed_skill
- After repo create/update in fctool: spawn embed_repo
- Wire EmbedService through AppService, available for all triggers
This commit is contained in:
ZhenYi 2026-04-28 13:04:04 +08:00
parent 93ec515f29
commit 62727a93a1
4 changed files with 75 additions and 0 deletions

View File

@ -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,

View File

@ -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))
}

View File

@ -37,6 +37,7 @@ pub struct AppService {
pub queue_producer: MessageProducer,
pub storage: Option<AppStorage>,
pub push: Option<WebPushService>,
pub embed_service: Option<Arc<EmbedService>>,
}
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<Arc<ChatService>> =
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,
})
}

View File

@ -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))
}