feat(ai): Phase 4 - multilingual, voice, A/B testing, analytics (with stubs)
Some checks failed
build-and-push / detect-changes (push) Successful in 6s
build-and-push / build (catering-services) (push) Failing after 5s
build-and-push / build (companies) (push) Failing after 6s
build-and-push / build (cron) (push) Failing after 5s
build-and-push / build (customers) (push) Failing after 5s
build-and-push / build (developers) (push) Failing after 5s
build-and-push / build (employees) (push) Successful in 11s
build-and-push / build (fitness-trainers) (push) Successful in 11s
build-and-push / build (graphic-designers) (push) Successful in 12s
build-and-push / build (gateway) (push) Successful in 25s
build-and-push / build (jobs) (push) Failing after 18s
build-and-push / build (leads) (push) Failing after 19s
build-and-push / build (makeup-artists) (push) Failing after 4s
build-and-push / build (payments) (push) Failing after 4s
build-and-push / build (photographers) (push) Failing after 18s
build-and-push / build (job-seekers) (push) Failing after 1m30s
build-and-push / build (tutors) (push) Failing after 5s
build-and-push / build (social-media-managers) (push) Failing after 20s
build-and-push / build (ugc-content-creators) (push) Failing after 5s
build-and-push / build (users) (push) Failing after 6s
build-and-push / build (video-editors) (push) Failing after 5s
Some checks failed
build-and-push / detect-changes (push) Successful in 6s
build-and-push / build (catering-services) (push) Failing after 5s
build-and-push / build (companies) (push) Failing after 6s
build-and-push / build (cron) (push) Failing after 5s
build-and-push / build (customers) (push) Failing after 5s
build-and-push / build (developers) (push) Failing after 5s
build-and-push / build (employees) (push) Successful in 11s
build-and-push / build (fitness-trainers) (push) Successful in 11s
build-and-push / build (graphic-designers) (push) Successful in 12s
build-and-push / build (gateway) (push) Successful in 25s
build-and-push / build (jobs) (push) Failing after 18s
build-and-push / build (leads) (push) Failing after 19s
build-and-push / build (makeup-artists) (push) Failing after 4s
build-and-push / build (payments) (push) Failing after 4s
build-and-push / build (photographers) (push) Failing after 18s
build-and-push / build (job-seekers) (push) Failing after 1m30s
build-and-push / build (tutors) (push) Failing after 5s
build-and-push / build (social-media-managers) (push) Failing after 20s
build-and-push / build (ugc-content-creators) (push) Failing after 5s
build-and-push / build (users) (push) Failing after 6s
build-and-push / build (video-editors) (push) Failing after 5s
This commit is contained in:
parent
088e467e58
commit
d48983ee21
10 changed files with 1590 additions and 1 deletions
|
|
@ -3450,6 +3450,8 @@ pub fn ai_router() -> Router<AppState> {
|
||||||
.route("/feedback", post(phase3::ai_feedback))
|
.route("/feedback", post(phase3::ai_feedback))
|
||||||
.route("/usage/v2", get(phase3::ai_usage))
|
.route("/usage/v2", get(phase3::ai_usage))
|
||||||
.route("/clear-history", axum::routing::post(phase3::ai_clear_history))
|
.route("/clear-history", axum::routing::post(phase3::ai_clear_history))
|
||||||
|
// ── Phase 4: multi-lang, voice, A/B, analytics, model swap, KB+ ───
|
||||||
|
.merge(crate::handlers::ai_phase4::phase4_router())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
1422
apps/users/src/handlers/ai_phase4.rs
Normal file
1422
apps/users/src/handlers/ai_phase4.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@ pub mod activity_logs;
|
||||||
pub mod approvals;
|
pub mod approvals;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod ai;
|
pub mod ai;
|
||||||
|
pub mod ai_phase4;
|
||||||
pub mod ai_prompts;
|
pub mod ai_prompts;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod coupons;
|
pub mod coupons;
|
||||||
|
|
|
||||||
|
|
@ -124,5 +124,5 @@ async fn main() {
|
||||||
tracing::info!("Users service listening on {}", addr);
|
tracing::info!("Users service listening on {}", addr);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
axum::serve(listener, app).await.unwrap();
|
let app = axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Phase 4: Add language/voice/tokens columns to ai_conversations.
|
||||||
|
-- These are non-destructive ALTERs with defaults so they are safe to run on
|
||||||
|
-- an existing database.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE ai_conversations
|
||||||
|
DROP COLUMN IF EXISTS language,
|
||||||
|
DROP COLUMN IF EXISTS has_voice,
|
||||||
|
DROP COLUMN IF EXISTS voice_duration_ms,
|
||||||
|
DROP COLUMN IF EXISTS tokens_estimate,
|
||||||
|
DROP COLUMN IF EXISTS summary;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
32
crates/db/migrations/20260608020000_ai_phase4_columns.up.sql
Normal file
32
crates/db/migrations/20260608020000_ai_phase4_columns.up.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- Phase 4: Add language/voice/tokens columns to ai_conversations.
|
||||||
|
-- All non-destructive with defaults so this is safe on existing rows.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. Detected language of the user message. 2-letter ISO code (en, es, fr,
|
||||||
|
-- de, hi, zh, ja, pt, ar, ru). Defaults to 'en' for back-compat.
|
||||||
|
ALTER TABLE ai_conversations
|
||||||
|
ADD COLUMN IF NOT EXISTS language VARCHAR(8) NOT NULL DEFAULT 'en';
|
||||||
|
|
||||||
|
-- 2. Voice-input metadata: did this conversation come from a voice
|
||||||
|
-- transcript, and how long was the audio?
|
||||||
|
ALTER TABLE ai_conversations
|
||||||
|
ADD COLUMN IF NOT EXISTS has_voice BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS voice_duration_ms INTEGER;
|
||||||
|
|
||||||
|
-- 3. Context-window management: a rough char-based token estimate for the
|
||||||
|
-- query+response pair, plus a one-line LLM-generated summary that we can
|
||||||
|
-- splice in when the conversation gets long.
|
||||||
|
ALTER TABLE ai_conversations
|
||||||
|
ADD COLUMN IF NOT EXISTS tokens_estimate INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS summary TEXT;
|
||||||
|
|
||||||
|
-- Hot path: "all conversations in a given language for analytics"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_conversations_language
|
||||||
|
ON ai_conversations (language, created_at DESC);
|
||||||
|
|
||||||
|
-- Hot path: "find voice conversations"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_conversations_voice
|
||||||
|
ON ai_conversations (user_id, has_voice) WHERE has_voice = TRUE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
10
crates/db/migrations/20260608030000_ai_ab_tests.down.sql
Normal file
10
crates/db/migrations/20260608030000_ai_ab_tests.down.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- Phase 4 rollback: drop A/B testing tables.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_ai_ab_assignments_variant;
|
||||||
|
DROP INDEX IF EXISTS idx_ai_ab_assignments_user;
|
||||||
|
DROP TABLE IF EXISTS ai_ab_assignments;
|
||||||
|
DROP TABLE IF EXISTS ai_ab_tests;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
40
crates/db/migrations/20260608030000_ai_ab_tests.up.sql
Normal file
40
crates/db/migrations/20260608030000_ai_ab_tests.up.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
-- Phase 4: A/B testing tables for Ask Ash prompt variations.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_ab_tests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
-- Each variant is an arbitrary JSON blob the request handler decides how
|
||||||
|
-- to interpret (e.g. {"persona":"job_seekers","system_prompt_override":"..."})
|
||||||
|
variant_a JSONB NOT NULL,
|
||||||
|
variant_b JSONB NOT NULL,
|
||||||
|
-- 0.0 → all traffic to A, 1.0 → all traffic to B, 0.5 → 50/50.
|
||||||
|
-- DOUBLE PRECISION because sqlx's default feature set doesn't include
|
||||||
|
-- bigdecimal/rust_decimal; double has plenty of precision for 0-1.
|
||||||
|
traffic_split DOUBLE PRECISION NOT NULL DEFAULT 0.50
|
||||||
|
CHECK (traffic_split >= 0.0 AND traffic_split <= 1.0),
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_ab_assignments (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
test_id BIGINT NOT NULL REFERENCES ai_ab_tests(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
-- References ai_conversations.id which is UUID.
|
||||||
|
conversation_id UUID REFERENCES ai_conversations(id) ON DELETE SET NULL,
|
||||||
|
variant TEXT NOT NULL CHECK (variant IN ('A', 'B')),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Hot path: "what variant was user X assigned to test Y?"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_ab_assignments_user
|
||||||
|
ON ai_ab_assignments (user_id, test_id);
|
||||||
|
|
||||||
|
-- Hot path: aggregate results per test
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_ab_assignments_variant
|
||||||
|
ON ai_ab_assignments (test_id, variant, created_at DESC);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
14
crates/db/migrations/20260608040000_ai_analytics.down.sql
Normal file
14
crates/db/migrations/20260608040000_ai_analytics.down.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Phase 4 rollback: drop analytics, user prefs, and article views.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_ai_article_views_user;
|
||||||
|
DROP INDEX IF EXISTS idx_ai_article_views_article;
|
||||||
|
DROP TABLE IF EXISTS ai_article_views;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS ai_user_prefs;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_ai_daily_stats_date;
|
||||||
|
DROP TABLE IF EXISTS ai_daily_stats;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
54
crates/db/migrations/20260608040000_ai_analytics.up.sql
Normal file
54
crates/db/migrations/20260608040000_ai_analytics.up.sql
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
-- Phase 4: Analytics rollup tables + per-user AI prefs + KB article views.
|
||||||
|
-- The aggregator job in ai_phase4::aggregate_daily_stats rolls up
|
||||||
|
-- ai_conversations into ai_daily_stats every 5 minutes.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Daily rollup: one row per (date, user, persona, pillar, intent).
|
||||||
|
-- All metrics come from ai_conversations + ai_feedback, never the raw rows.
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_daily_stats (
|
||||||
|
date DATE NOT NULL,
|
||||||
|
user_id UUID,
|
||||||
|
persona TEXT,
|
||||||
|
pillar TEXT,
|
||||||
|
intent TEXT,
|
||||||
|
request_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
kb_match_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ollama_calls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
fallback_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
avg_response_ms INTEGER,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (date, user_id, persona, pillar, intent)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_daily_stats_date
|
||||||
|
ON ai_daily_stats (date DESC);
|
||||||
|
|
||||||
|
-- Per-user AI preferences (e.g. preferred model, language override).
|
||||||
|
-- Kept small and simple; one row per user.
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_user_prefs (
|
||||||
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
preferred_model TEXT,
|
||||||
|
preferred_language VARCHAR(8) NOT NULL DEFAULT 'en',
|
||||||
|
-- Bump when this user changes their prefs so the clients can re-fetch.
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Track which KB articles users actually look at after Ask Ash surfaces
|
||||||
|
-- them. Used by the enhanced KB matcher for recency boosting.
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_article_views (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
-- kb_articles.id is UUID.
|
||||||
|
article_id UUID NOT NULL REFERENCES kb_articles(id) ON DELETE CASCADE,
|
||||||
|
helpful BOOLEAN,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_article_views_article
|
||||||
|
ON ai_article_views (article_id, created_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_article_views_user
|
||||||
|
ON ai_article_views (user_id, created_at DESC);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
Add table
Reference in a new issue