Skip to content

AI trong sản phẩm — @tasco/ai

@tasco/ai là nền tảng để app sản phẩm thêm tính năng AI (chat, tóm tắt, trích xuất…) theo đúng kiến trúc framework: client không bao giờ giữ API key, mọi request đi qua BFF.

Browser ──POST /api/ai/chat──▶ Nitro BFF (createAiChatProxy) ──Bearer key──▶ AI Gateway (OpenAI-compatible)

Gateway nội bộ (LiteLLM) do team hạ tầng vận hành — app chỉ cần 2 biến env.

Cài đặt

bash
pnpm add @tasco/ai

nuxt.config.ts của app (giống @tasco/ui/@tasco/composables trong layer):

ts
build: { transpile: ['@tasco/ai'] }

.env (key do team hạ tầng cấp theo app — không dùng chung, không commit):

bash
AI_GATEWAY_URL=https://ai-gateway.example.com/v1
AI_GATEWAY_KEY=sk-...

Server — mount proxy

Tạo server/api/ai/chat.post.ts:

ts
import { createAiChatProxy } from '@tasco/ai/server'

export default createAiChatProxy({
  gatewayUrl: process.env.AI_GATEWAY_URL!,
  apiKey: process.env.AI_GATEWAY_KEY!,
  defaultModel: 'claude-sonnet-5',
  allowedModels: ['claude-sonnet-5', 'claude-haiku-4-5'],
})
OptionMặc địnhÝ nghĩa
gatewayUrl / apiKey— (bắt buộc)Endpoint + key gateway, chỉ tồn tại server-side
defaultModelModel khi client không gửi
allowedModelscho tất cảAllowlist model
maxMessages50Số message tối đa mỗi request
maxTotalChars100_000Tổng ký tự tối đa
authorizekhông checkHook (event) => boolean — trả false → 401

Bảo vệ có sẵn: body bị whitelist (chỉ model/messages/temperature/max_tokens đi qua, message chỉ giữ role + content); lỗi key gateway (401/403) trả về client dạng 502 chung chung — không lộ chi tiết cấu hình.

Client — useAiChat

vue
<script setup lang="ts">
import { useAiChat } from '@tasco/ai'
import { tascoAssistant, renderPrompt } from '@tasco/ai/prompts'

const { messages, status, error, send, abort, reset } = useAiChat({
  system: renderPrompt(tascoAssistant, { app_name: 'Thu phí ETC' }),
})
</script>
Trả vềÝ nghĩa
messagesRef<ChatMessage[]> — cập nhật dần theo stream SSE, render trực tiếp
statusidle / streaming / error — disable input khi streaming
errorAppError khi lỗi thật (placeholder rỗng tự gỡ, message user giữ lại để gửi lại)
send(text)Gửi — tự chặn khi đang streaming, bỏ qua input rỗng
abort()Dừng stream, giữ phần đã nhận
reset()Xoá hội thoại về trạng thái đầu

Chỉ dùng client-side (page framework mặc định CSR — xem Kiến trúc).

UI — CChat + CChatMessage

@tasco/ui có sẵn component chat theo theme Tasco, nối thẳng với useAiChat:

vue
<template>
  <CChat
    :messages="messages"
    :status="status"
    :error="error?.message"
    title="Trợ lý Tasco"
    @send="send"
    @stop="abort"
    @clear="reset"
  />
</template>
  • CChat: khung hội thoại + ô nhập (Enter gửi, Shift+Enter xuống dòng, không cắt ngang IME tiếng Việt), nút Gửi ↔ Dừng theo status, prop disabled để khoá (chưa đăng nhập/hết quota).
  • CChatMessage: bong bóng theo role, pending hiện chấm gõ khi chờ token đầu.
  • Props, sự kiện, slot và demo chạy thật: CChat, CChatMessage.

Prompt library — @tasco/ai/prompts

Prompt dùng chung có version, template {{biến}} validate 2 chiều (thiếu biến khai báo hoặc placeholder không được cấp → ném lỗi ngay lúc dev):

ts
import { definePrompt, renderPrompt } from '@tasco/ai/prompts'

const myPrompt = definePrompt({
  id: 'orders-triage',
  version: 1,
  description: 'Phân loại yêu cầu hỗ trợ đơn hàng',
  template: 'Bạn hỗ trợ app {{app_name}}...',
  variables: ['app_name'],
})
const system = renderPrompt(myPrompt, { app_name: 'Thu phí ETC' })

Có sẵn: tascoAssistant (persona trợ lý chuẩn — tiếng Việt, không bịa số liệu), summarize, extractJson. Quy tắc: đổi nội dung template → tăng version + thêm eval case.

Eval — @tasco/ai/eval

Golden cases ở packages/ai/evals/cases.ts, assertion tất định (contains / regex / json-valid / max-lines… — không dùng LLM chấm LLM):

bash
AI_GATEWAY_URL=... AI_GATEWAY_KEY=... pnpm --filter @tasco/ai eval

CI: job ai-eval tự chạy khi MR đụng packages/ai/** và khi merge vào main (allow_failure — gateway sập không khoá MR). Cần biến CI/CD AI_GATEWAY_URL / AI_GATEWAY_KEY, tuỳ chọn AI_EVAL_MODEL.