Skip to content
Open
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
5 changes: 5 additions & 0 deletions apps/builder/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,11 @@
"description": "The domains or subdomains that are authorized to access your webchat.",
"label": "Authorized Domain{plural, select, 1 {s} other {}}"
},
"webSearchAuthorizedDomains": {
"label": "Authorized websites for Web Search tool",
"placeholder": "example.com",
"tooltip": "List of domains that the AI agent can access. Leave empty to allow all domains."
},
"authToken": {
"label": "Token"
},
Expand Down
5 changes: 5 additions & 0 deletions apps/builder/messages/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@
"description": "Các tên miền hoặc tên miền phụ được ủy quyền truy cập webchat của bạn.",
"label": "Tên miền được ủy quyền{plural, select, 1 {} other {}}"
},
"webSearchAuthorizedDomains": {
"label": "Website được phép cho công cụ Tìm kiếm web",
"placeholder": "example.com",
"tooltip": "Danh sách domain AI agent được phép truy cập. Để trống để cho phép tất cả domain."
},
"authToken": {
"label": "Token"
},
Expand Down
17 changes: 15 additions & 2 deletions apps/builder/src/features/ai-agents/ai-agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { withCache } from "@chatbotx.io/redis"
import { createId } from "@chatbotx.io/utils"
import type { PaginatedResponse } from "@/features/common/schemas/pagination"
import { BaseService } from "../common/base.service"
import { normalizeWebSearchDomains } from "./lib/web-search-tool"
import type {
CreateAIAgentRequest,
UpdateAIAgentRequest,
Expand Down Expand Up @@ -107,8 +108,12 @@ class AiAgentService extends BaseService {
.set({ isDefault: false })
.where(eq(aiAgentModel.workspaceId, workspaceId))
}
const { webSearchAuthorizedDomains, ...rest } = data
await client.insert(aiAgentModel).values({
...data,
...rest,
webSearchAuthorizedDomains: normalizeWebSearchDomains(
webSearchAuthorizedDomains,
),
workspaceId,
id: createId(),
})
Expand Down Expand Up @@ -142,9 +147,17 @@ class AiAgentService extends BaseService {
.set({ isDefault: false })
.where(eq(aiAgentModel.workspaceId, ctx.workspaceId))
}
const { webSearchAuthorizedDomains, ...rest } = data
await tx
.update(aiAgentModel)
.set(data)
.set({
...rest,
...(webSearchAuthorizedDomains !== undefined && {
webSearchAuthorizedDomains: normalizeWebSearchDomains(
webSearchAuthorizedDomains,
),
}),
})
.where(eq(aiAgentModel.id, aiAgent.id))
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client"

import { InputField } from "@chatbotx.io/ui/components/form/input-field"
import { Button } from "@chatbotx.io/ui/components/ui/button"
import { Label } from "@chatbotx.io/ui/components/ui/label"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@chatbotx.io/ui/components/ui/tooltip"
import { InfoIcon, PlusIcon, XIcon } from "lucide-react"
import { useTranslations } from "next-intl"
import { useCallback, useEffect } from "react"
import { useFieldArray, useFormContext, useWatch } from "react-hook-form"
import {
isWebSearchSelected,
MAX_WEB_SEARCH_AUTHORIZED_DOMAINS,
} from "../lib/web-search-tool"

export function WebSearchAuthorizedDomainsField() {
const t = useTranslations()
const { control } = useFormContext()
const tools = useWatch({ control, name: "tools" }) as string[] | undefined
const {
fields: authorizedDomains,
append,
remove,
replace,
} = useFieldArray({
control,
name: "webSearchAuthorizedDomains",
})

const hasWebSearch = isWebSearchSelected(tools)
const hasReachedLimit =
authorizedDomains.length >= MAX_WEB_SEARCH_AUTHORIZED_DOMAINS

useEffect(() => {
if (!hasWebSearch && authorizedDomains.length > 0) {
replace([])
}
}, [authorizedDomains.length, hasWebSearch, replace])

const handleAddDomain = useCallback(() => {
if (!hasReachedLimit) {
append({ value: "" })
}
}, [append, hasReachedLimit])

if (!hasWebSearch) {
return null
}

return (
<div className="space-y-4 rounded-md border border-input p-4">
<div className="flex items-center gap-2">
<Label htmlFor="webSearchAuthorizedDomains">
{t("fields.webSearchAuthorizedDomains.label")}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
aria-label={t("fields.webSearchAuthorizedDomains.tooltip")}
className="size-4 text-muted-foreground"
/>
</TooltipTrigger>
<TooltipContent className="max-w-sm">
{t("fields.webSearchAuthorizedDomains.tooltip")}
</TooltipContent>
</Tooltip>
</div>

<div className="space-y-2">
{authorizedDomains.map((field, index) => (
<div className="flex gap-2" key={field.id}>
<InputField
name={`webSearchAuthorizedDomains.${index}.value`}
placeholder={t("fields.webSearchAuthorizedDomains.placeholder")}
/>

<Button
aria-label={t("actions.delete")}
onClick={() => remove(index)}
size="icon"
type="button"
variant="ghost"
>
<XIcon aria-hidden className="size-4" />
</Button>
</div>
))}
</div>

<Button
className="w-full"
disabled={hasReachedLimit}
onClick={handleAddDomain}
type="button"
variant="outline"
>
<PlusIcon aria-hidden className="size-4" />
{t("actions.addNew")}
</Button>
</div>
)
}
3 changes: 3 additions & 0 deletions apps/builder/src/features/ai-agents/create-ai-agent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
createAIAgentRequest,
} from "@/features/ai-agents/schemas/action"
import { AIToolMultiSelect } from "@/features/ai-tools/components/ai-tool-multi-select"
import { WebSearchAuthorizedDomainsField } from "./components/web-search-authorized-domains-field"

type CreateAIAgentDialogProps = {
workspaceId: string
Expand Down Expand Up @@ -95,6 +96,7 @@ export function CreateAIAgentDialog({
temperature: 0.4,
maxOutputTokens: 2048,
tools: [],
webSearchAuthorizedDomains: [],
},
},
errorMapProps: {},
Expand Down Expand Up @@ -246,6 +248,7 @@ export function CreateAIAgentDialog({
</div>

<AIToolMultiSelect name="tools" />
<WebSearchAuthorizedDomainsField />

<DialogFooter className="justify-end gap-2 sm:gap-2">
<Button
Expand Down
35 changes: 35 additions & 0 deletions apps/builder/src/features/ai-agents/lib/web-search-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { systemFunctionNames, toolPrefixes } from "@chatbotx.io/ai"
import { z } from "zod"

export const MAX_WEB_SEARCH_AUTHORIZED_DOMAINS = 20

const authorizedWebSearchDomainSchema = z.hostname()

export const webSearchToolValue = `${toolPrefixes.enum.sys}:${systemFunctionNames.webSearch}`

type WebSearchAuthorizedDomain = {
value: string
}

export function isWebSearchSelected(tools?: string[] | null): boolean {
return Boolean(tools?.includes(webSearchToolValue))
}

export function normalizeWebSearchDomains(
domains?: WebSearchAuthorizedDomain[] | null,
): string[] {
const normalizedDomains = new Set<string>()

for (const domain of domains ?? []) {
const normalizedDomain = domain.value.trim().toLowerCase()

if (
normalizedDomain &&
authorizedWebSearchDomainSchema.safeParse(normalizedDomain).success
) {
normalizedDomains.add(normalizedDomain)
}
}

return Array.from(normalizedDomains)
}
9 changes: 9 additions & 0 deletions apps/builder/src/features/ai-agents/schemas/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@chatbotx.io/ai"
import { aiMessageRoles } from "@chatbotx.io/database/partials"
import { z } from "zod"
import { MAX_WEB_SEARCH_AUTHORIZED_DOMAINS } from "../lib/web-search-tool"

export const createAIAgentRequest = z.object({
name: z.string().trim().min(1).max(255),
Expand Down Expand Up @@ -40,6 +41,14 @@ export const createAIAgentRequest = z.object({
temperature: z.number().min(0).max(2),
maxOutputTokens: z.number().min(1).max(32_768),
tools: z.array(z.string()),
webSearchAuthorizedDomains: z
.array(
z.object({
value: z.string().trim().pipe(z.hostname()),
}),
)
.max(MAX_WEB_SEARCH_AUTHORIZED_DOMAINS)
.default([]),
isDefault: z.boolean(),
})
export type CreateAIAgentRequest = z.infer<typeof createAIAgentRequest>
Expand Down
6 changes: 6 additions & 0 deletions apps/builder/src/features/ai-agents/update-ai-agent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
updateAIAgentRequest,
} from "@/features/ai-agents/schemas/action"
import { AIToolMultiSelect } from "@/features/ai-tools/components/ai-tool-multi-select"
import { WebSearchAuthorizedDomainsField } from "./components/web-search-authorized-domains-field"

export function UpdateAIAgentDialog({
workspaceId,
Expand Down Expand Up @@ -136,6 +137,10 @@ export function UpdateAIAgentDialog({
setValue("maxOutputTokens", agent.maxOutputTokens)
setValue("messages", agent.messages as UpdateAIAgentRequest["messages"])
setValue("tools", agent.tools)
setValue(
"webSearchAuthorizedDomains",
agent.webSearchAuthorizedDomains.map((domain) => ({ value: domain })),
)
}
}, [agent, setValue])

Expand Down Expand Up @@ -251,6 +256,7 @@ export function UpdateAIAgentDialog({
</div>

<AIToolMultiSelect name="tools" />
<WebSearchAuthorizedDomainsField />

<DialogFooter className="justify-end gap-2 sm:gap-2">
<Button
Expand Down
12 changes: 5 additions & 7 deletions apps/builder/src/features/ai-tools/provider/ai-tools-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { systemFunctionNames } from "@chatbotx.io/ai"
import { systemFunctionCatalog } from "@chatbotx.io/ai"
import ky, { HTTPError } from "ky"
import { createStore } from "zustand/vanilla"
import type { ListAIFilesResponse } from "@/features/ai-files/schemas"
Expand Down Expand Up @@ -40,12 +40,10 @@ export const createAIToolsStore = (props: Pick<AIToolsState, "workspaceId">) =>
files: [],
functions: [],
mcpServers: [],
systemFunctions: [
{
id: systemFunctionNames.connectUserToHuman,
name: systemFunctionNames.connectUserToHuman,
},
],
systemFunctions: Object.values(systemFunctionCatalog).map((item) => ({
id: item.id,
name: item.id,
})),

initialize: async () => {
if (get().initialized) {
Expand Down
3 changes: 3 additions & 0 deletions apps/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@chatbotx.io/variables": "workspace:*",
"@chatbotx.io/worker-config": "workspace:*",
"@faker-js/faker": "^10.4.0",
"@mozilla/readability": "^0.6.0",
"@platformatic/kafka": "^1.34.0",
"@t3-oss/env-core": "^0.13.11",
"ai": "^6.0.170",
Expand All @@ -70,6 +71,7 @@
"html-to-text": "^9.0.5",
"image-size": "^2.0.2",
"ioredis": "5.10.1",
"jsdom": "^25.0.1",
"jszip": "^3.10.1",
"ky": "^2.0.2",
"libphonenumber-js": "^1.12.42",
Expand All @@ -88,6 +90,7 @@
"@chatbotx.io/typescript-config": "workspace:*",
"@chatbotx.io/vitest-config": "workspace:*",
"@types/html-to-text": "^9.0.4",
"@types/jsdom": "^28.0.3",
"@types/mailparser": "^3.4.6",
"@types/mime-types": "^3.0.1",
"@types/node": "24.x",
Expand Down
Loading
Loading