feat: token count implement
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
-- Question Bank: Centralized repository for reusable questions
|
||||
-- Supports multiple question types, audio generation, and multi-tenant organization
|
||||
|
||||
-- Question types supported by the platform
|
||||
CREATE TYPE question_bank_type AS ENUM (
|
||||
'multiple-choice', -- Multiple choice with single/multiple correct answers
|
||||
'true-false', -- True/False questions
|
||||
'short-answer', -- Short text answer
|
||||
'essay', -- Long form text answer
|
||||
'matching', -- Match pairs
|
||||
'ordering', -- Order items correctly
|
||||
'fill-in-the-blanks', -- Fill in missing words
|
||||
'audio-response', -- Record audio answer
|
||||
'hotspot', -- Click on image area
|
||||
'code-lab' -- Code exercise
|
||||
);
|
||||
|
||||
-- Question Bank table
|
||||
CREATE TABLE question_bank (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
|
||||
-- Question content
|
||||
question_text TEXT NOT NULL,
|
||||
question_type question_bank_type NOT NULL,
|
||||
|
||||
-- Answers and options (structure depends on question_type)
|
||||
options JSONB, -- Array of options for multiple-choice/true-false
|
||||
correct_answer JSONB, -- Correct answer(s) - format varies by type
|
||||
explanation TEXT, -- Explanation shown after answering (AI generated or manual)
|
||||
|
||||
-- Audio support (Bark TTS)
|
||||
audio_url TEXT, -- URL to generated audio file
|
||||
audio_text TEXT, -- Text used for audio generation (may differ from question_text)
|
||||
audio_status VARCHAR(50) DEFAULT 'pending', -- pending, generating, ready, failed
|
||||
audio_metadata JSONB, -- Bark generation parameters
|
||||
|
||||
-- Media support
|
||||
media_url TEXT, -- Image/video URL for hotspot or context questions
|
||||
media_type VARCHAR(50), -- image, video
|
||||
|
||||
-- Metadata
|
||||
points INTEGER NOT NULL DEFAULT 1,
|
||||
difficulty VARCHAR(20) DEFAULT 'medium', -- easy, medium, hard
|
||||
tags TEXT[], -- For searching and categorization
|
||||
skill_assessed VARCHAR(20), -- reading, listening, speaking, writing
|
||||
|
||||
-- Source tracking
|
||||
source VARCHAR(50) DEFAULT 'manual', -- manual, ai-generated, imported-mysql, imported-csv
|
||||
source_metadata JSONB, -- Original source data (e.g., MySQL question ID)
|
||||
imported_mysql_id INTEGER, -- Original MySQL question ID to prevent re-import
|
||||
imported_mysql_course_id INTEGER, -- Original MySQL course ID
|
||||
|
||||
-- Usage tracking
|
||||
usage_count INTEGER DEFAULT 0, -- How many times used in templates/courses
|
||||
last_used_at TIMESTAMPTZ,
|
||||
|
||||
-- Status
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_archived BOOLEAN DEFAULT false,
|
||||
|
||||
-- Audit
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT question_bank_points_positive CHECK (points > 0)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_question_bank_org ON question_bank(organization_id);
|
||||
CREATE INDEX idx_question_bank_type ON question_bank(question_type);
|
||||
CREATE INDEX idx_question_bank_difficulty ON question_bank(difficulty);
|
||||
CREATE INDEX idx_question_bank_tags ON question_bank USING GIN(tags);
|
||||
CREATE INDEX idx_question_bank_source ON question_bank(source);
|
||||
CREATE INDEX idx_question_bank_active ON question_bank(is_active) WHERE is_active = true;
|
||||
CREATE INDEX idx_question_bank_search ON question_bank USING GIN(to_tsvector('english', question_text));
|
||||
|
||||
-- Question Bank Categories (optional organization)
|
||||
CREATE TABLE question_bank_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id UUID REFERENCES question_bank_categories(id) ON DELETE CASCADE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
UNIQUE(organization_id, name)
|
||||
);
|
||||
|
||||
-- Link questions to categories
|
||||
CREATE TABLE question_bank_question_categories (
|
||||
question_id UUID NOT NULL REFERENCES question_bank(id) ON DELETE CASCADE,
|
||||
category_id UUID NOT NULL REFERENCES question_bank_categories(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (question_id, category_id)
|
||||
);
|
||||
|
||||
-- Question Bank Usage History (track where questions are used)
|
||||
CREATE TABLE question_bank_usage (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
question_id UUID NOT NULL REFERENCES question_bank(id) ON DELETE CASCADE,
|
||||
used_in_type VARCHAR(50) NOT NULL, -- template, lesson, quiz
|
||||
used_in_id UUID NOT NULL, -- ID of the template/lesson/quiz
|
||||
organization_id UUID NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_question_bank_usage_question ON question_bank_usage(question_id);
|
||||
CREATE INDEX idx_question_bank_usage_used_in ON question_bank_usage(used_in_type, used_in_id);
|
||||
|
||||
-- Trigger to update updated_at
|
||||
CREATE TRIGGER trg_question_bank_updated_at
|
||||
BEFORE UPDATE ON question_bank
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_test_templates_updated_at(); -- Reuse existing function
|
||||
|
||||
-- Function to increment usage count
|
||||
CREATE OR REPLACE FUNCTION increment_question_usage(p_question_id UUID, p_used_in_type VARCHAR, p_used_in_id UUID, p_org_id UUID, p_user_id UUID)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Update usage count
|
||||
UPDATE question_bank
|
||||
SET usage_count = usage_count + 1,
|
||||
last_used_at = NOW()
|
||||
WHERE id = p_question_id;
|
||||
|
||||
-- Log usage
|
||||
INSERT INTO question_bank_usage (question_id, used_in_type, used_in_id, organization_id, created_by)
|
||||
VALUES (p_question_id, p_used_in_type, p_used_in_id, p_org_id, p_user_id);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to import question from MySQL
|
||||
CREATE OR REPLACE FUNCTION import_question_from_mysql(
|
||||
p_org_id UUID,
|
||||
p_user_id UUID,
|
||||
p_question_text TEXT,
|
||||
p_question_type question_bank_type,
|
||||
p_options JSONB,
|
||||
p_correct_answer JSONB,
|
||||
p_source_metadata JSONB
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_question_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO question_bank (
|
||||
organization_id, created_by, question_text, question_type,
|
||||
options, correct_answer, source, source_metadata,
|
||||
audio_status, is_active
|
||||
)
|
||||
VALUES (
|
||||
p_org_id, p_user_id, p_question_text, p_question_type,
|
||||
p_options, p_correct_answer, 'imported-mysql', p_source_metadata,
|
||||
'pending', true
|
||||
)
|
||||
RETURNING id INTO v_question_id;
|
||||
|
||||
RETURN v_question_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE question_bank IS 'Centralized repository for reusable questions across the organization';
|
||||
COMMENT ON COLUMN question_bank.question_type IS 'Type of question: multiple-choice, true-false, short-answer, essay, matching, ordering, fill-in-the-blanks, audio-response, hotspot, code-lab';
|
||||
COMMENT ON COLUMN question_bank.audio_url IS 'URL to Bark-generated audio file for text-to-speech';
|
||||
COMMENT ON COLUMN question_bank.audio_status IS 'Status of audio generation: pending, generating, ready, failed';
|
||||
COMMENT ON COLUMN question_bank.source IS 'Origin: manual (created by user), ai-generated, imported-mysql, imported-csv';
|
||||
COMMENT ON COLUMN question_bank.source_metadata IS 'Original source data, e.g., {mysql_table: "bancopreguntas", idPregunta: 123, idCursos: 456}';
|
||||
COMMENT ON TABLE question_bank_usage IS 'Tracks where each question is used (templates, lessons, quizzes)';
|
||||
@@ -0,0 +1,86 @@
|
||||
-- AI Usage Logs: Track token consumption per user for billing and limits
|
||||
CREATE TABLE ai_usage_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
|
||||
-- Token counts
|
||||
tokens_used INTEGER NOT NULL DEFAULT 0,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Request metadata
|
||||
endpoint VARCHAR(255), -- e.g., "/lessons/generate-quiz"
|
||||
model VARCHAR(100), -- e.g., "llama3.2:3b", "gpt-4o"
|
||||
request_type VARCHAR(50), -- "transcription", "quiz-generation", "chat", "summary"
|
||||
request_metadata JSONB, -- Additional context about the request
|
||||
|
||||
-- Cost estimation (optional, can be calculated later)
|
||||
estimated_cost_usd NUMERIC(10, 6),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_ai_usage_user ON ai_usage_logs(user_id);
|
||||
CREATE INDEX idx_ai_usage_org ON ai_usage_logs(organization_id);
|
||||
CREATE INDEX idx_ai_usage_created ON ai_usage_logs(created_at);
|
||||
CREATE INDEX idx_ai_usage_endpoint ON ai_usage_logs(endpoint);
|
||||
CREATE INDEX idx_ai_usage_type ON ai_usage_logs(request_type);
|
||||
|
||||
-- Function to log AI usage
|
||||
CREATE OR REPLACE FUNCTION log_ai_usage(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_tokens INTEGER,
|
||||
p_input_tokens INTEGER,
|
||||
p_output_tokens INTEGER,
|
||||
p_endpoint VARCHAR,
|
||||
p_model VARCHAR,
|
||||
p_request_type VARCHAR,
|
||||
p_metadata JSONB
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_log_id UUID;
|
||||
v_cost NUMERIC(10, 6);
|
||||
BEGIN
|
||||
-- Calculate estimated cost (OpenAI-like pricing)
|
||||
v_cost := (p_input_tokens::NUMERIC * 0.000001) + (p_output_tokens::NUMERIC * 0.000003);
|
||||
|
||||
INSERT INTO ai_usage_logs (
|
||||
user_id, organization_id, tokens_used, input_tokens, output_tokens,
|
||||
endpoint, model, request_type, request_metadata, estimated_cost_usd
|
||||
)
|
||||
VALUES (
|
||||
p_user_id, p_org_id, p_tokens, p_input_tokens, p_output_tokens,
|
||||
p_endpoint, p_model, p_request_type, p_metadata, v_cost
|
||||
)
|
||||
RETURNING id INTO v_log_id;
|
||||
|
||||
RETURN v_log_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Daily aggregation view for reporting
|
||||
CREATE VIEW ai_usage_daily AS
|
||||
SELECT
|
||||
DATE(created_at) as usage_date,
|
||||
user_id,
|
||||
organization_id,
|
||||
request_type,
|
||||
SUM(tokens_used) as total_tokens,
|
||||
SUM(input_tokens) as total_input,
|
||||
SUM(output_tokens) as total_output,
|
||||
COUNT(*) as request_count,
|
||||
SUM(estimated_cost_usd) as total_cost
|
||||
FROM ai_usage_logs
|
||||
GROUP BY DATE(created_at), user_id, organization_id, request_type;
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE ai_usage_logs IS 'Tracks AI token usage per user for billing, limits, and analytics';
|
||||
COMMENT ON COLUMN ai_usage_logs.tokens_used IS 'Total tokens (input + output)';
|
||||
COMMENT ON COLUMN ai_usage_logs.input_tokens IS 'Tokens in the prompt/request';
|
||||
COMMENT ON COLUMN ai_usage_logs.output_tokens IS 'Tokens in the AI response';
|
||||
COMMENT ON COLUMN ai_usage_logs.estimated_cost_usd IS 'Estimated cost based on OpenAI-like pricing';
|
||||
Reference in New Issue
Block a user