Fix /api/ai/* returning 500 on every request, close credit-minting bugs, add grounding guardrail
Critical: ai_access_middleware was wired via from_fn_with_state((), ...) - passing the unit type as state - and pulled AppState from request extensions, which nothing ever populated. Every request through /api/ai/* and /api/ai/auto/* returned 500 INCOMPLETE_CONTEXT. Fixed by extracting State<AppState> properly and passing the real state at both call sites; removed the redundant, identically-broken inner middleware layer inside ai_router(). Security: ai_addon_purchase (/api/ai/addons/purchase, /api/ai/credits/buy) and ai_plan_upgrade (/api/ai/plans/upgrade) granted AI credits / plan upgrades (including enterprise) with zero payment verification - any authenticated user could mint unlimited free credits, and the frontend already called this directly. Disabled both until wired to a real payment flow. Quality: added a grounding/anti-hallucination system prompt applied to every AI feature call (orchestrator::call_feature / call_feature_with_plan, plus the handful of call sites that bypass the orchestrator). Verified against the live model that it reduces but does not eliminate fabrication on harder reasoning tasks - even the larger model invents facts not present in the input on some prompts. This is a real limitation of the two locally-hosted models, not something a system prompt alone fully solves; flagged for follow-up (e.g. a verification pass or deterministic checks for high-stakes decisions like auto-apply). Also fixed Persona/Pillar keyword detection using naive substring matching (e.g. "team" matching inside "esteemed", "lead" matching inside "leadership") - added a word-boundary-aware contains_word() helper and applied it to all keyword classifiers in this file.
This commit is contained in:
parent
64089e4da3
commit
eaae3d470f
6 changed files with 134 additions and 155 deletions
|
|
@ -133,24 +133,13 @@ impl IntoResponse for AiAccessError {
|
|||
/// AI access. Applies to all routes under `/api/ai/*`. Non-customer roles that
|
||||
/// have no explicit subscription will automatically get a Free plan.
|
||||
pub async fn ai_access_middleware(
|
||||
State(_state): State<()>,
|
||||
State(state): State<AppState>,
|
||||
auth: AuthUser,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AiAccessError> {
|
||||
let role_code = auth.claims.active_role.clone();
|
||||
|
||||
// State is not available directly in axum middleware; access the pool
|
||||
// via the request extensions where AppState was installed by `with_state`.
|
||||
let state = request
|
||||
.extensions()
|
||||
.get::<AppState>()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("AppState not found in request extensions");
|
||||
AiAccessError::IncompleteContext
|
||||
})?;
|
||||
|
||||
let (_sub, _plan) = plans::ensure_free_subscription(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,30 @@ use crate::ai::{credits, litellm, model_router, plans, usage};
|
|||
use db::models::ai::AiFeatureCost;
|
||||
use crate::AppState;
|
||||
|
||||
/// Baseline anti-hallucination / grounding instructions applied to every AI
|
||||
/// feature call, regardless of what feature-specific system prompt (if any)
|
||||
/// the caller supplies. Added after confirming the model will otherwise
|
||||
/// confidently fabricate skills/qualifications not present in the input
|
||||
/// (e.g. asserting a candidate has AWS/Kubernetes experience when only
|
||||
/// Python/SQL were listed) and use that fabrication to justify its answer.
|
||||
pub const GROUNDING_GUARDRAIL: &str = "You are an AI assistant for the Nxtgauge platform. Follow these rules strictly: \
|
||||
(1) Only use facts, skills, experience, or qualifications explicitly stated in the input below - never invent, \
|
||||
assume, or infer skills, experience, credentials, or requirements that were not explicitly mentioned. \
|
||||
(2) If information needed to fully answer is missing, say so explicitly rather than filling the gap with an assumption. \
|
||||
(3) When evaluating fit, gaps, matches, or recommendations, be honest about mismatches and missing requirements - \
|
||||
do not default to an encouraging or positive tone if the input doesn't support it. \
|
||||
(4) Do not fabricate specific facts, numbers, dates, or names not present in the input. \
|
||||
(5) Keep responses concise and directly relevant to the request.";
|
||||
|
||||
/// Combine the baseline grounding guardrail with an optional feature-specific
|
||||
/// system prompt.
|
||||
pub fn effective_system_prompt(feature_prompt: Option<&str>) -> String {
|
||||
match feature_prompt {
|
||||
Some(p) if !p.trim().is_empty() => format!("{}\n\n{}", GROUNDING_GUARDRAIL, p),
|
||||
_ => GROUNDING_GUARDRAIL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AiCallError {
|
||||
#[error("Plan error: {0}")]
|
||||
|
|
@ -98,7 +122,7 @@ pub async fn call_feature(
|
|||
let (text, usage, request_id) = client
|
||||
.chat_completion_text(
|
||||
&model_alias,
|
||||
system_prompt,
|
||||
Some(&effective_system_prompt(system_prompt)),
|
||||
user_message,
|
||||
max_tokens,
|
||||
)
|
||||
|
|
@ -182,7 +206,7 @@ pub async fn call_feature_with_plan(
|
|||
let (text, usage, request_id) = client
|
||||
.chat_completion_text(
|
||||
&model_alias,
|
||||
system_prompt,
|
||||
Some(&effective_system_prompt(system_prompt)),
|
||||
user_message,
|
||||
max_tokens,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::ai::{credits, litellm, model_router, plans, usage};
|
||||
use crate::ai::{credits, litellm, model_router, orchestrator, plans, usage};
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
|
|
@ -229,7 +229,7 @@ async fn admin_ai_call(
|
|||
};
|
||||
|
||||
let (text, usage, request_id) = match client
|
||||
.chat_completion_text(&model_alias, None, prompt, None)
|
||||
.chat_completion_text(&model_alias, Some(&orchestrator::effective_system_prompt(None)), prompt, None)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"what is ", "what are ", "where do i find", "where can i find",
|
||||
"search for", "find article", "look up",
|
||||
];
|
||||
if HELP_KW.iter().any(|k| m.contains(k)) {
|
||||
if HELP_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("help_search", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"having trouble with", "issue with", "problem with", "complaint",
|
||||
"refund request", "cancel my account", "billing issue", "billing problem",
|
||||
];
|
||||
if TICKET_KW.iter().any(|k| m.contains(k)) {
|
||||
if TICKET_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("ticket_creation", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"extract from", "extract fields", "extract info", "extract information",
|
||||
"autofill", "auto-fill", "parse this form", "from this text",
|
||||
];
|
||||
if FORM_KW.iter().any(|k| m.contains(k)) {
|
||||
if FORM_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("form_filling", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"draft a job description", "job description for", "jd for", "job posting for",
|
||||
"write job description", "generate job description",
|
||||
];
|
||||
if JD_KW.iter().any(|k| m.contains(k)) {
|
||||
if JD_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("job_description_generation", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"cover letter", "coverletter", "write a letter", "application letter",
|
||||
"letter of interest", "motivation letter",
|
||||
];
|
||||
if CL_KW.iter().any(|k| m.contains(k)) {
|
||||
if CL_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("generate_cover_letter", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"update my resume", "update resume", "fix my resume", "optimize my resume",
|
||||
"customize my resume", "adjust my resume", "polish my resume",
|
||||
];
|
||||
if RESUME_KW.iter().any(|k| m.contains(k)) {
|
||||
if RESUME_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("improve_resume", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"unlock lead", "unlock contact", "lead contact", "view lead",
|
||||
"request to view",
|
||||
];
|
||||
if CONTACT_KW.iter().any(|k| m.contains(k)) {
|
||||
if CONTACT_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("request_view_contact", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ fn classify_strict_keywords(message: &str) -> Option<(&'static str, f32)> {
|
|||
"auto apply", "auto-apply", "apply to all", "apply for me",
|
||||
"apply on my behalf", "apply automatically", "bulk apply", "mass apply",
|
||||
];
|
||||
if APPLY_KW.iter().any(|k| m.contains(k)) {
|
||||
if APPLY_KW.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(("auto_apply_job", 0.95));
|
||||
}
|
||||
|
||||
|
|
@ -614,7 +614,7 @@ async fn ai_chat_generate(
|
|||
}
|
||||
};
|
||||
return match client
|
||||
.chat_completion_text("askash-fast", None, prompt, None)
|
||||
.chat_completion_text("askash-fast", Some(&orchestrator::effective_system_prompt(None)), prompt, None)
|
||||
.await
|
||||
{
|
||||
Ok((text, _, _)) => (text, true),
|
||||
|
|
@ -689,7 +689,7 @@ async fn ai_chat_generate(
|
|||
|
||||
let (text, usage, request_id) = match client
|
||||
.chat_completion_text(&model_alias,
|
||||
None,
|
||||
Some(&orchestrator::effective_system_prompt(None)),
|
||||
prompt,
|
||||
None,
|
||||
)
|
||||
|
|
@ -1871,6 +1871,57 @@ async fn ai_usage_status(
|
|||
// - DISCOVER : help find things (search, recommendations)
|
||||
// - IMPROVE : optimize existing (analytics, suggestions)
|
||||
|
||||
/// Word-boundary aware substring check, so a keyword like "team" doesn't
|
||||
/// match inside unrelated words like "steam" or "esteemed", and "lead"
|
||||
/// doesn't match inside "leadership". Keywords with a trailing/leading
|
||||
/// space (e.g. "make a") are treated literally; the check still applies
|
||||
/// boundary rules to the overall match span.
|
||||
fn contains_word(haystack: &str, needle: &str) -> bool {
|
||||
let bytes = haystack.as_bytes();
|
||||
let needle_bytes = needle.as_bytes();
|
||||
if needle_bytes.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_boundary = |c: Option<u8>| match c {
|
||||
None => true,
|
||||
Some(b) => !(b as char).is_alphanumeric(),
|
||||
};
|
||||
|
||||
// Only enforce a boundary on an edge if the needle itself is alphanumeric
|
||||
// there — a needle like "how do i " already carries its own boundary via
|
||||
// the trailing space, so requiring another non-alphanumeric char after
|
||||
// that would over-constrain it.
|
||||
let needle_starts_alnum = (needle_bytes[0] as char).is_alphanumeric();
|
||||
let needle_ends_alnum = (needle_bytes[needle_bytes.len() - 1] as char).is_alphanumeric();
|
||||
|
||||
let mut start = 0;
|
||||
while let Some(pos) = haystack[start..].find(needle) {
|
||||
let match_start = start + pos;
|
||||
let match_end = match_start + needle_bytes.len();
|
||||
|
||||
let before_ok = !needle_starts_alnum || {
|
||||
let before = if match_start == 0 { None } else { Some(bytes[match_start - 1]) };
|
||||
is_boundary(before)
|
||||
};
|
||||
let after_ok = !needle_ends_alnum || {
|
||||
let after = bytes.get(match_end).copied();
|
||||
is_boundary(after)
|
||||
};
|
||||
|
||||
if before_ok && after_ok {
|
||||
return true;
|
||||
}
|
||||
|
||||
start = match_start + 1;
|
||||
if start >= haystack.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Persona {
|
||||
|
|
@ -1901,7 +1952,7 @@ impl Persona {
|
|||
"recruitment", "team", "employer", "organization", "org ", "staff",
|
||||
"headcount", "workforce", "b2b", "enterprise",
|
||||
];
|
||||
if COMPANIES.iter().any(|k| m.contains(k)) {
|
||||
if COMPANIES.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Persona::Companies);
|
||||
}
|
||||
|
||||
|
|
@ -1911,7 +1962,7 @@ impl Persona {
|
|||
"resume", "cv ", "interview", "hiring me", "salary", "offer letter",
|
||||
"job board", "job listing", "vacancy", "position", "candidate",
|
||||
];
|
||||
if JOB_SEEKERS.iter().any(|k| m.contains(k)) {
|
||||
if JOB_SEEKERS.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Persona::JobSeekers);
|
||||
}
|
||||
|
||||
|
|
@ -1921,7 +1972,7 @@ impl Persona {
|
|||
"quote", "quotation", "order", "checkout", "payment", "invoice me",
|
||||
"subscription", "plan", "package",
|
||||
];
|
||||
if CUSTOMERS.iter().any(|k| m.contains(k)) {
|
||||
if CUSTOMERS.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Persona::Customers);
|
||||
}
|
||||
|
||||
|
|
@ -1931,7 +1982,7 @@ impl Persona {
|
|||
"freelancer", "consultant", "contractor", "side hustle", "service provider",
|
||||
"lead", "leads", "client", "project", "deliverable",
|
||||
];
|
||||
if PROFESSIONALS.iter().any(|k| m.contains(k)) {
|
||||
if PROFESSIONALS.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Persona::Professionals);
|
||||
}
|
||||
|
||||
|
|
@ -1968,7 +2019,7 @@ impl Pillar {
|
|||
"generate", "new ", "add a", "set up", "setup ", "post a",
|
||||
"publish", "start a", "begin a", "launch",
|
||||
];
|
||||
if CREATE.iter().any(|k| m.contains(k)) {
|
||||
if CREATE.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Pillar::Create);
|
||||
}
|
||||
|
||||
|
|
@ -1978,7 +2029,7 @@ impl Pillar {
|
|||
"verification", "onboard", "onboarding", "fill in", "fill out",
|
||||
"resume setup", "complete my", "finish my", "pick up where",
|
||||
];
|
||||
if COMPLETE.iter().any(|k| m.contains(k)) {
|
||||
if COMPLETE.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Pillar::Complete);
|
||||
}
|
||||
|
||||
|
|
@ -1988,7 +2039,7 @@ impl Pillar {
|
|||
"suggest", "show me", "browse", "discover", "explore", "best",
|
||||
"top ", "near me", "nearby", "available",
|
||||
];
|
||||
if DISCOVER.iter().any(|k| m.contains(k)) {
|
||||
if DISCOVER.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Pillar::Discover);
|
||||
}
|
||||
|
||||
|
|
@ -1998,7 +2049,7 @@ impl Pillar {
|
|||
"performance", "metrics", "stats", "statistics", "better", "enhance",
|
||||
"upgrade", "polish", "refine", "tweak", "fix my",
|
||||
];
|
||||
if IMPROVE.iter().any(|k| m.contains(k)) {
|
||||
if IMPROVE.iter().any(|k| contains_word(&m, k)) {
|
||||
return Some(Pillar::Improve);
|
||||
}
|
||||
|
||||
|
|
@ -2122,7 +2173,7 @@ fn is_support_intent(message: &str) -> bool {
|
|||
"can't", "cant ", "cannot", "unable to", "issue", "problem",
|
||||
"help me fix", "stuck", "blocked",
|
||||
];
|
||||
SUPPORT_KW.iter().any(|k| m.contains(k))
|
||||
SUPPORT_KW.iter().any(|k| contains_word(&m, k))
|
||||
}
|
||||
|
||||
// ── Phase 2: auto-create a support ticket for support-intent queries ──────────
|
||||
|
|
@ -3873,121 +3924,23 @@ pub mod phase3 {
|
|||
}
|
||||
|
||||
/// POST /api/ai/addons/purchase — purchase a credit pack.
|
||||
///
|
||||
/// This previously granted credits with no payment verification at all -
|
||||
/// any authenticated user could mint unlimited free AI credits. Disabled
|
||||
/// until it's wired to a real payment flow (see the PayU-verified
|
||||
/// nxtgauge-rust-payments ai_credits flow for the pattern to follow).
|
||||
pub async fn ai_addon_purchase(
|
||||
State(state): State<AppState>,
|
||||
auth: contracts::auth_middleware::AuthUser,
|
||||
Json(body): Json<AddonPurchaseBody>,
|
||||
State(_state): State<AppState>,
|
||||
_auth: contracts::auth_middleware::AuthUser,
|
||||
Json(_body): Json<AddonPurchaseBody>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
let code = body.addon_code.trim().to_uppercase();
|
||||
let addon_amount = match code.as_str() {
|
||||
"STARTER" => 50,
|
||||
"GROWTH" => 150,
|
||||
"POWER" => 500,
|
||||
"ENTERPRISE" => 1000,
|
||||
_ => {
|
||||
match AiCreditPackageRepository::list_active(&state.pool).await {
|
||||
Ok(packages) => packages
|
||||
.into_iter()
|
||||
.find(|pkg| {
|
||||
pkg.name
|
||||
.to_uppercase()
|
||||
.replace([' ', '-'], "_")
|
||||
.contains(&code)
|
||||
})
|
||||
.map(|pkg| pkg.credits),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to list AI credit packages: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or(0)
|
||||
}
|
||||
};
|
||||
|
||||
if addon_amount <= 0 {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
axum::Json(serde_json::json!({
|
||||
"success": false,
|
||||
"message": format!("Unknown addon code: {}", body.addon_code),
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let _ = match crate::ai::plans::ensure_free_subscription(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
Some(&auth.claims.active_role),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
return (e.status_code(), axum::Json(e.error_body())).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = UserAiSubscriptionRepository::add_purchased_credits(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
addon_amount,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("ai_addon_purchase update failed: {}", e);
|
||||
return (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(AddonPurchaseResponse {
|
||||
success: false,
|
||||
addon_balance: 0,
|
||||
message: "Failed to process purchase".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let Some(updated_sub) = UserAiSubscriptionRepository::get_by_user_id(&state.pool, auth.user_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten() else {
|
||||
return (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(AddonPurchaseResponse {
|
||||
success: false,
|
||||
addon_balance: 0,
|
||||
message: "Subscription not found after purchase".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
let purchased_remaining =
|
||||
(updated_sub.purchased_credits_total - updated_sub.purchased_credits_used).max(0);
|
||||
let remaining_credits = crate::ai::credits::remaining_credits(&updated_sub);
|
||||
|
||||
if let Err(e) = AiCreditTransactionRepository::create(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
"credit",
|
||||
"purchase",
|
||||
addon_amount,
|
||||
remaining_credits,
|
||||
None,
|
||||
Some(&format!("addon_code={}", body.addon_code)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to record AI credit purchase transaction: {}", e);
|
||||
}
|
||||
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
axum::Json(AddonPurchaseResponse {
|
||||
success: true,
|
||||
addon_balance: purchased_remaining,
|
||||
message: format!("Successfully added {} AI credits", addon_amount),
|
||||
}),
|
||||
axum::http::StatusCode::PAYMENT_REQUIRED,
|
||||
axum::Json(serde_json::json!({
|
||||
"success": false,
|
||||
"message": "Direct credit purchase is temporarily unavailable. Please use the AI credits checkout flow.",
|
||||
"code": "PAYMENT_VERIFICATION_REQUIRED",
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
|
@ -4006,11 +3959,28 @@ pub mod phase3 {
|
|||
}
|
||||
|
||||
/// POST /api/ai/plans/upgrade — upgrade the user's AI plan.
|
||||
///
|
||||
/// This previously upgraded to ANY plan (including enterprise) with no
|
||||
/// payment verification at all - only "free" is allowed here now, since
|
||||
/// there is no pricing/payment linkage for paid plans in this table yet.
|
||||
pub async fn ai_plan_upgrade(
|
||||
State(state): State<AppState>,
|
||||
auth: contracts::auth_middleware::AuthUser,
|
||||
Json(body): Json<PlanUpgradeBody>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
if !body.plan_code.trim().eq_ignore_ascii_case("free") {
|
||||
return (
|
||||
axum::http::StatusCode::PAYMENT_REQUIRED,
|
||||
axum::Json(PlanUpgradeResponse {
|
||||
success: false,
|
||||
plan: "Free".to_string(),
|
||||
monthly_limit: 10,
|
||||
message: "Paid plan upgrades require a verified payment and are not yet available through this endpoint.".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let plan = match AiPlanRepository::get_by_code(&state.pool, &body.plan_code).await {
|
||||
Ok(Some(plan)) => plan,
|
||||
Ok(None) => {
|
||||
|
|
@ -4443,10 +4413,6 @@ pub fn ai_router() -> Router<AppState> {
|
|||
.route("/plans/upgrade", post(phase3::ai_plan_upgrade))
|
||||
.merge(crate::handlers::ai_auto::ai_auto_router())
|
||||
.merge(crate::handlers::ai_phase4::phase4_router())
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
(),
|
||||
crate::ai::middleware::ai_access_middleware,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::ai::{credits, litellm, model_router, plans, usage};
|
||||
use crate::ai::{credits, litellm, model_router, orchestrator, plans, usage};
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
|
|
@ -306,7 +306,7 @@ async fn ai_suggest(
|
|||
|
||||
let full_prompt = format!("{}\n\nPreferences/Context: {}\n\nSuggestions:", system_prompt, text);
|
||||
let (response_text, usage, request_id) = match client
|
||||
.chat_completion_text(&model_alias, None, &full_prompt, None)
|
||||
.chat_completion_text(&model_alias, Some(&orchestrator::effective_system_prompt(None)), &full_prompt, None)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
|
|
|
|||
|
|
@ -119,11 +119,11 @@ async fn main() {
|
|||
.nest("/api/admin/ai", handlers::admin_ai::admin_ai_router())
|
||||
// ── AI Assistant ──────────────────────────────────────────────────
|
||||
.nest("/api/ai", handlers::ai::ai_router().layer(axum::middleware::from_fn_with_state(
|
||||
(),
|
||||
state.clone(),
|
||||
crate::ai::middleware::ai_access_middleware,
|
||||
)))
|
||||
.nest("/api/ai/auto", handlers::ai_auto::ai_auto_router().layer(axum::middleware::from_fn_with_state(
|
||||
(),
|
||||
state.clone(),
|
||||
crate::ai::middleware::ai_access_middleware,
|
||||
)))
|
||||
.route("/health", get(|| async { "Users OK" }))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue