Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .agents/skills/worker-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ Workers run as separate Node processes in `apps/worker/`. They consume jobs from
| integration | `integration` | `src/integration/worker.ts` |
| chat | `chat` | `src/chat/worker.ts` |
| ai-agent | `aiAgent` | `src/ai-agent/worker.ts` |
| heavy | `heavy` | `src/heavy/worker.ts` |
| default | `default` | `src/default/worker.ts` |
| trigger | `trigger` | `src/trigger/worker.ts` |
| webhook | `webhook` | `src/webhook/worker.ts` |
| schedule | (cron) | `src/schedule/worker.ts` |
| sequence-scheduler | Kafka | `src/sequence-scheduler/worker*.ts` |
| notification | `notification` | `src/notification/worker.ts` |

The `heavy` queue/worker is a **workload-class** queue, not a domain queue:
use it for bounded but RAM/CPU/I/O/model-heavy jobs that should not occupy
latency-sensitive domain workers. AI file processing, media generation,
speech/text conversion, document extraction, and image analysis are current
tenants. Future heavy workloads can join this queue with their own
`src/heavy/handlers/<domain-or-capability>/` handler area when the same
resource-isolation tradeoff applies.

## Creating a New Queue

### 1. Define Queue Name
Expand Down
28 changes: 26 additions & 2 deletions apps/builder/__tests__/ai-files-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ vi.mock("@chatbotx.io/utils", () => ({
}))

vi.mock("@chatbotx.io/worker-config", () => ({
AIJobAction: { processAIFile: "processAIFile" },
aiAgentQueue: { add: mocks.queueAdd },
HeavyJobAction: { processAIFile: "processAIFile" },
getHeavyJobOptions: () => ({}),
heavyQueue: { add: mocks.queueAdd },
}))

vi.mock("next-intl/server", () => ({
Expand Down Expand Up @@ -104,6 +105,21 @@ beforeEach(() => {
})

describe("Knowledge tab audit messages", () => {
test("allows creating a Knowledge with Gemini as the only provider", async () => {
mocks.findFirstOpenai.mockResolvedValue(undefined)
mocks.findFirstGemini.mockResolvedValue({ id: "gemini-1" })

await (
createAIFileAction as unknown as ActionHandler<{ name: string }, [string]>
)({
parsedInput: { name: "manual.pdf" },
bindArgsParsedInputs: [workspaceId],
})

expect(mocks.insertReturning).toHaveBeenCalled()
expect(mocks.queueAdd).toHaveBeenCalled()
})

test("createAIFileAction logs created a new Knowledge by id", async () => {
await (
createAIFileAction as unknown as ActionHandler<{ name: string }, [string]>
Expand All @@ -117,6 +133,14 @@ describe("Knowledge tab audit messages", () => {
action: "create",
detail: "created a new Knowledge (#file-1)",
})
expect(mocks.queueAdd).toHaveBeenCalledWith(
"processAIFile",
{
type: "processAIFile",
data: { aiFileId: "file-1" },
},
{ jobId: "heavy-ai-file-file-1" },
)
})

test("deleteAIFile logs deleted a Knowledge by id", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/builder/__tests__/workspace-owner-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ vi.mock("@chatbotx.io/utils", () => {
return () => proxy
},
})
return { zodBigintAsString: () => proxy }
return { zodBigintAsString: () => proxy, zodUrlWithVariables: () => proxy }
})

vi.mock("@/env", () => ({ isCloud }))
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "يتجاوز حجم الملف الحد الأقصى",
"maxItemsReached": "الحد الأقصى المسموح به لكل مساحة عمل هو {max} من {feature}",
"maxCharacters": "الحد الأقصى {max} حرفًا.",
"invalidApiKey": "مفتاح API غير صالح",
"maxMustBeGreaterThanMin": "يجب أن يكون {maxField} أكبر من أو يساوي {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Fil størrelse exceeds maksimum limit",
"maxItemsReached": "Maksimum {max} {feature} allowed per arbejdsområde",
"maxCharacters": "Maksimalt {max} tegn.",
"invalidApiKey": "Ugyldig API-nøgle",
"maxMustBeGreaterThanMin": "{maxField} must være greater than eller equal til {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Die Dateigröße überschreitet das Höchstlimit",
"maxItemsReached": "Pro Arbeitsbereich sind höchstens {max} {feature} zulässig",
"maxCharacters": "Maximal {max} Zeichen.",
"invalidApiKey": "Ungültiger API-Schlüssel",
"maxMustBeGreaterThanMin": "{maxField} muss größer als oder gleich {minField} sein"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3586,6 +3586,7 @@
"validation": {
"maxSize": "File size exceeds maximum limit",
"maxItemsReached": "Maximum {max} {feature} allowed per workspace",
"maxCharacters": "Maximum {max} characters.",
"invalidApiKey": "Invalid API key",
"maxMustBeGreaterThanMin": "{maxField} must be greater than or equal to {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Archivo size exceeds maximum limit",
"maxItemsReached": "Maximum {max} {feature} todosowed per espacio de trabajo",
"maxCharacters": "Máximo {max} caracteres.",
"invalidApiKey": "No válido API clave",
"maxMustBeGreaterThanMin": "{maxField} must be greater than o equal un {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Tiedoston koko ylittää enimmäisrajan",
"maxItemsReached": "Työtilassa sallitaan enintään {max} kohdetta {feature}",
"maxCharacters": "Enintään {max} merkkiä.",
"invalidApiKey": "Virheellinen API-avain",
"maxMustBeGreaterThanMin": "Kentän {maxField} arvon on oltava vähintään kentän {minField} arvo"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "La taille du fichier dépasse la limite maximale",
"maxItemsReached": "Maximum de {max} {feature} autorisé par espace de travail",
"maxCharacters": "Maximum {max} caractères.",
"invalidApiKey": "Clé API non valide",
"maxMustBeGreaterThanMin": "{maxField} doit être supérieur ou égal à {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,7 @@
"validation": {
"maxSize": "גודל הקובץ חורג מהמגבלה המרבית",
"maxItemsReached": "מותר לכל היותר {max} {feature} בכל סביבת עבודה",
"maxCharacters": "מקסימום {max} תווים.",
"invalidApiKey": "מפתח API לא תקין",
"maxMustBeGreaterThanMin": "{maxField} חייב להיות גדול או שווה ל-{minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Ukuran file melebihi batas maksimum",
"maxItemsReached": "Maksimum {max} {feature} diizinkan per ruang kerja",
"maxCharacters": "Maksimum {max} karakter.",
"invalidApiKey": "Kunci API tidak valid",
"maxMustBeGreaterThanMin": "{maxField} harus lebih besar dari atau sama dengan {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "La dimensione del file supera il limite massimo",
"maxItemsReached": "Sono consentiti al massimo {max} {feature} per area di lavoro",
"maxCharacters": "Massimo {max} caratteri.",
"invalidApiKey": "Chiave API non valida",
"maxMustBeGreaterThanMin": "{maxField} deve essere maggiore o uguale a {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -3378,6 +3378,7 @@
"validation": {
"maxSize": "ファイルサイズが上限を超えています",
"maxItemsReached": "ワークスペースごとに許可される{feature}は最大{max}件です",
"maxCharacters": "最大{max}文字です。",
"invalidApiKey": "APIキーが無効です",
"maxMustBeGreaterThanMin": "{maxField}は{minField}以上である必要があります"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "De bestandsgrootte overschrijdt de maximale limiet",
"maxItemsReached": "Maximaal {max} {feature} toegestaan per workspace",
"maxCharacters": "Maximaal {max} tekens.",
"invalidApiKey": "Ongeldige API-sleutel",
"maxMustBeGreaterThanMin": "{maxField} moet groter zijn dan of gelijk zijn aan {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -3378,6 +3378,7 @@
"validation": {
"maxSize": "O tamanho do arquivo excede o limite máximo",
"maxItemsReached": "Máximo de {max} {feature} permitido por espaço de trabalho",
"maxCharacters": "Máximo de {max} caracteres.",
"invalidApiKey": "Chave de API inválida",
"maxMustBeGreaterThanMin": "{maxField} deve ser maior ou igual a {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/pt-PT.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "O tamanho do ficheiro excede o limite máximo",
"maxItemsReached": "É permitido um máximo de {max} {feature} por espaço de trabalho",
"maxCharacters": "Máximo de {max} caracteres.",
"invalidApiKey": "Chave da API inválida",
"maxMustBeGreaterThanMin": "{maxField} tem de ser maior ou igual a {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Dimensiunea fișierului depășește limita maximă",
"maxItemsReached": "Sunt permise maximum {max} {feature} per spațiu de lucru",
"maxCharacters": "Maximum {max} caractere.",
"invalidApiKey": "Cheie API invalidă",
"maxMustBeGreaterThanMin": "{maxField} trebuie să fie mai mare sau egal cu {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -4706,6 +4706,7 @@
"validation": {
"invalidApiKey": "Ogiltig API-nyckel",
"maxItemsReached": "Högst {max} {feature} tillåts per arbetsyta",
"maxCharacters": "Högst {max} tecken.",
"maxMustBeGreaterThanMin": "{maxField} måste vara större än eller lika med {minField}",
"maxSize": "Filstorleken överskrider maxgränsen"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Dosya boyutu izin verilen maksimum sınırı aşıyor",
"maxItemsReached": "Çalışma alanı başına en fazla {max} {feature} izin verilir",
"maxCharacters": "En fazla {max} karakter.",
"invalidApiKey": "Geçersiz API anahtarı",
"maxMustBeGreaterThanMin": "{maxField}, {minField} değerinden büyük veya ona eşit olmalıdır"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@
"validation": {
"maxSize": "Kích thước tệp vượt quá giới hạn tối đa",
"maxItemsReached": "Tối đa {max} {feature} cho mỗi workspace",
"maxCharacters": "Tối đa {max} ký tự.",
"invalidApiKey": "API key không hợp lệ",
"maxMustBeGreaterThanMin": "{maxField} phải lớn hơn hoặc bằng {minField}"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -4706,6 +4706,7 @@
"validation": {
"invalidApiKey": "API 密钥无效",
"maxItemsReached": "最大 {max}{feature}",
"maxCharacters": "最多 {max} 个字符。",
"maxMustBeGreaterThanMin": "{maxField} 必须大于或等于 {minField}",
"maxSize": "文件大小超出上限"
},
Expand Down
1 change: 1 addition & 0 deletions apps/builder/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -3432,6 +3432,7 @@
"validation": {
"maxSize": "檔案大小超出上限",
"maxItemsReached": "最大 {max}{feature}",
"maxCharacters": "最多 {max} 個字元。",
"invalidApiKey": "API 金鑰無效",
"maxMustBeGreaterThanMin": "{maxField} 必須大於或等於 {minField}"
},
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/src/app/developer/queues/[[...path]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
chatQueue,
defaultQueue,
getSequenceSchedulerQueue,
heavyQueue,
integrationQueue,
quotaQueue,
scheduleQueue,
Expand Down Expand Up @@ -48,6 +49,7 @@ async function buildApp() {
const queues = [
chatQueue,
aiAgentQueue,
heavyQueue,
triggerQueue,
webhookQueue,
defaultQueue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { ChatbotXException } from "@chatbotx.io/business/errors"
import { db } from "@chatbotx.io/database/client"
import { aiFileModel } from "@chatbotx.io/database/schema"
import { createId } from "@chatbotx.io/utils"
import { AIJobAction, aiAgentQueue } from "@chatbotx.io/worker-config"
import {
getHeavyJobOptions,
HeavyJobAction,
heavyQueue,
} from "@chatbotx.io/worker-config"
import { getTranslations } from "next-intl/server"
import { workspaceIdrequestParams } from "@/features/common/schema"
import { workspaceActionClient } from "@/lib/safe-action"
Expand Down Expand Up @@ -41,12 +45,19 @@ export const createAIFileAction = workspaceActionClient
.returning({ id: aiFileModel.id })

// Enqueue embedding job right after creation
await aiAgentQueue.add(AIJobAction.processAIFile, {
type: AIJobAction.processAIFile,
data: {
aiFileId: created[0].id,
await heavyQueue.add(
HeavyJobAction.processAIFile,
{
type: HeavyJobAction.processAIFile,
data: {
aiFileId: created[0].id,
},
},
})
{
...getHeavyJobOptions(HeavyJobAction.processAIFile),
jobId: `heavy-ai-file-${created[0].id}`,
},
)

await auditService.record({
workspaceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ export const AIModelDialog = ({ parentName }: AIModelDialogProps) => {
getValues: getParentValues,
setValue: setParentValue,
} = useFormContext()
const provider = useWatch({ name: `${parentName}.provider`, control })

const form = useForm({
resolver: zodResolver(aiGenerateImageSchema),
defaultValues: getParentValues(parentName),
})
const provider = useWatch({ name: `${parentName}.provider`, control })
const model = useWatch({ control: form.control, name: "model" })

useEffect(() => {
if (!open) {
Expand Down Expand Up @@ -96,7 +97,7 @@ export const AIModelDialog = ({ parentName }: AIModelDialogProps) => {

{isOpenAI && <QualitySelect name="quality" />}

<SizeSelect name="size" provider={provider} />
<SizeSelect model={model ?? ""} name="size" provider={provider} />

<CustomFieldSelect
allowCreate={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,45 @@ export const QualitySelect = (props: QualitySelectProps) => {

type SizeSelectProps = {
name: string
model: string
required?: boolean
provider: AIGenerateImageProvider
}

export const SizeSelect = (props: SizeSelectProps) => {
const { provider, ...rest } = props
const { model, provider, ...rest } = props
const t = useTranslations()

const isGPTImage =
model.startsWith("gpt-image") || model.startsWith("chatgpt-image")

const optionsMap = useMemo<Record<AIGenerateImageProvider, SelectOption[]>>(
() => ({
openai: [
{ label: t("fields.size.options.auto"), value: "auto" },
{ label: t("fields.size.options.square1024"), value: "1024x1024" },
{
label: t("fields.size.options.landscape1536x1024"),
value: "1536x1024",
},
{
label: t("fields.size.options.portrait1024x1536"),
value: "1024x1536",
},
{ label: t("fields.size.options.dalle2_256"), value: "256x256" },
{ label: t("fields.size.options.dalle2_512"), value: "512x512" },
{
label: t("fields.size.options.dalle3_1792x1024"),
value: "1792x1024",
},
...(isGPTImage
? [
{
label: t("fields.size.options.square1024"),
value: "1024x1024",
},
{
label: t("fields.size.options.landscape1536x1024"),
value: "1536x1024",
},
{
label: t("fields.size.options.portrait1024x1536"),
value: "1024x1536",
},
]
: [
{ label: t("fields.size.options.dalle2_256"), value: "256x256" },
{ label: t("fields.size.options.dalle2_512"), value: "512x512" },
{
label: t("fields.size.options.dalle3_1792x1024"),
value: "1792x1024",
},
]),
],
gemini: [
{ label: t("fields.size.options.auto"), value: "auto" },
Expand All @@ -69,7 +81,7 @@ export const SizeSelect = (props: SizeSelectProps) => {
{ label: "16:9", value: "16:9" },
],
}),
[t],
[isGPTImage, t],
)

return (
Expand Down
Loading