feat(kb): POST /api/admin/kb/articles/ai-draft endpoint
All checks were successful
build-and-release / build (tutors) (push) Successful in 9s
build-and-release / build (jobs) (push) Successful in 14s
build-and-release / build (catering-services) (push) Successful in 14s
build-and-release / build (ugc-content-creators) (push) Successful in 7s
build-and-release / build (video-editors) (push) Successful in 8s
backend-integration-tests / ai-credits (push) Successful in 7s
build-and-release / build (customers) (push) Successful in 14s
build-and-release / build (payments) (push) Successful in 13s
build-and-release / build (companies) (push) Successful in 17s
build-and-release / build (developers) (push) Successful in 16s
build-and-release / build (cron) (push) Successful in 22s
build-and-release / build (employees) (push) Successful in 21s
build-and-release / build (gateway) (push) Successful in 8s
build-and-release / build (graphic-designers) (push) Successful in 8s
build-and-release / build (users) (push) Successful in 2m55s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 8s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (social-media-managers) (push) Successful in 10s
build-and-release / build (photographers) (push) Successful in 10s
All checks were successful
build-and-release / build (tutors) (push) Successful in 9s
build-and-release / build (jobs) (push) Successful in 14s
build-and-release / build (catering-services) (push) Successful in 14s
build-and-release / build (ugc-content-creators) (push) Successful in 7s
build-and-release / build (video-editors) (push) Successful in 8s
backend-integration-tests / ai-credits (push) Successful in 7s
build-and-release / build (customers) (push) Successful in 14s
build-and-release / build (payments) (push) Successful in 13s
build-and-release / build (companies) (push) Successful in 17s
build-and-release / build (developers) (push) Successful in 16s
build-and-release / build (cron) (push) Successful in 22s
build-and-release / build (employees) (push) Successful in 21s
build-and-release / build (gateway) (push) Successful in 8s
build-and-release / build (graphic-designers) (push) Successful in 8s
build-and-release / build (users) (push) Successful in 2m55s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 8s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (social-media-managers) (push) Successful in 10s
build-and-release / build (photographers) (push) Successful in 10s
Generates a full KB article (content + summary) via LiteLLM given a
title, optional category name, and optional topic hints.
- Prompt instructs the LLM to produce markdown-formatted body (300-500
words) followed by ===SUMMARY=== delimiter and a ≤200-char summary
- Falls back gracefully if LLM omits the delimiter: derives summary from
the first non-heading sentence
- Charges kb_article_draft feature credits against the admin's account
- Route is /api/admin/kb/articles/ai-draft (POST) — must be registered
BEFORE /api/admin/kb/articles/{id} so the literal segment wins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ad4ab0477a
commit
b039ed7342
1 changed files with 94 additions and 1 deletions
|
|
@ -3,7 +3,7 @@ use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, patch},
|
routing::{get, patch, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use contracts::auth_middleware::AuthUser;
|
use contracts::auth_middleware::AuthUser;
|
||||||
|
|
@ -31,6 +31,7 @@ pub fn admin_router() -> Router<AppState> {
|
||||||
patch(admin_update_category).delete(admin_delete_category),
|
patch(admin_update_category).delete(admin_delete_category),
|
||||||
)
|
)
|
||||||
// Articles
|
// Articles
|
||||||
|
.route("/articles/ai-draft", post(admin_ai_draft_article))
|
||||||
.route("/articles", get(admin_list_articles).post(admin_create_article))
|
.route("/articles", get(admin_list_articles).post(admin_create_article))
|
||||||
.route(
|
.route(
|
||||||
"/articles/{id}",
|
"/articles/{id}",
|
||||||
|
|
@ -955,6 +956,98 @@ async fn admin_delete_article(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── POST /api/admin/kb/articles/ai-draft ─────────────────────────────────────
|
||||||
|
// Generates a full KB article draft (body + summary) from a title and optional
|
||||||
|
// topic hints. Admin reviews and saves with the normal create/update endpoints.
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AiDraftArticleBody {
|
||||||
|
title: String,
|
||||||
|
category: Option<String>,
|
||||||
|
topic_hints: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AiDraftArticleResponse {
|
||||||
|
content: String,
|
||||||
|
summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn admin_ai_draft_article(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(body): Json<AiDraftArticleBody>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let category = body.category.as_deref().unwrap_or("General");
|
||||||
|
let hints = body.topic_hints.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
let prompt = format!(
|
||||||
|
"You are a technical writer for Nxtgauge, a multi-vertical marketplace platform \
|
||||||
|
connecting job seekers, professionals, companies, and customers.\n\n\
|
||||||
|
Write a clear and helpful help center article.\n\
|
||||||
|
Title: {title}\n\
|
||||||
|
Category: {category}\n\
|
||||||
|
{hints_section}\
|
||||||
|
Requirements:\n\
|
||||||
|
- Write in plain, friendly language aimed at non-technical users\n\
|
||||||
|
- Use markdown: ## for section headings, **bold** for key terms, numbered or bullet lists where helpful\n\
|
||||||
|
- 300–500 words total\n\
|
||||||
|
- End with a short 'Need more help?' paragraph directing users to contact support\n\n\
|
||||||
|
Return ONLY the article body in markdown. Do NOT include the article title at the top.\n\n\
|
||||||
|
After the article body, add this exact delimiter on its own line:\n\
|
||||||
|
===SUMMARY===\n\
|
||||||
|
Then write a 1–2 sentence summary (under 200 characters) suitable for search results and previews.",
|
||||||
|
title = body.title,
|
||||||
|
category = category,
|
||||||
|
hints_section = if hints.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("Additional context: {hints}\n")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let outcome = crate::ai_credits::call_litellm_and_charge(
|
||||||
|
&state.pool,
|
||||||
|
auth.user_id,
|
||||||
|
"kb_article_draft",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&prompt,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match outcome {
|
||||||
|
Ok(outcome) => {
|
||||||
|
let raw = outcome.result.trim().to_string();
|
||||||
|
// Split on ===SUMMARY=== delimiter; fall back gracefully if LLM omits it
|
||||||
|
let (content, summary) = if let Some(idx) = raw.find("===SUMMARY===") {
|
||||||
|
let body_part = raw[..idx].trim().to_string();
|
||||||
|
let summary_part = raw[idx + "===SUMMARY===".len()..].trim().to_string();
|
||||||
|
(body_part, summary_part)
|
||||||
|
} else {
|
||||||
|
// No delimiter — derive a short summary from the first non-heading line
|
||||||
|
let first_sentence = raw
|
||||||
|
.lines()
|
||||||
|
.find(|l| !l.trim().is_empty() && !l.trim().starts_with('#'))
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.trim_end_matches('.')
|
||||||
|
.to_string();
|
||||||
|
(raw, format!("{first_sentence}."))
|
||||||
|
};
|
||||||
|
(StatusCode::OK, Json(AiDraftArticleResponse { content, summary })).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("admin_ai_draft_article failed: {}", e);
|
||||||
|
(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
Json(serde_json::json!({ "error": "AI draft generation failed", "detail": e.to_string() })),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Map target_roles array to a single frontend role label
|
/// Map target_roles array to a single frontend role label
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue