feat: add openai embedding service

This commit is contained in:
Abhishek Kumar 2026-01-17 13:36:26 +05:30
parent eb41285204
commit 3f0e500fde
39 changed files with 1902 additions and 339 deletions

View file

@ -7,8 +7,10 @@ import {
ReactFlow,
} from "@xyflow/react";
import { BrushCleaning, Maximize2, Minus, Plus, Rocket, Settings, Variable } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { listDocumentsApiV1KnowledgeBaseDocumentsGet, listToolsApiV1ToolsGet } from '@/client';
import type { DocumentResponseSchema, ToolResponse } from '@/client/types.gen';
import { FlowEdge, FlowNode, NodeType } from "@/components/flow/types";
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
@ -63,6 +65,8 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
const [isConfigurationsDialogOpen, setIsConfigurationsDialogOpen] = useState(false);
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
const [isPhoneCallDialogOpen, setIsPhoneCallDialogOpen] = useState(false);
const [documents, setDocuments] = useState<DocumentResponseSchema[] | undefined>(undefined);
const [tools, setTools] = useState<ToolResponse[] | undefined>(undefined);
const {
rfInstance,
@ -95,6 +99,36 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
getAccessToken
});
// Fetch documents and tools once for the entire workflow
useEffect(() => {
const fetchData = async () => {
try {
const accessToken = await getAccessToken();
// Fetch documents
const documentsResponse = await listDocumentsApiV1KnowledgeBaseDocumentsGet({
headers: { Authorization: `Bearer ${accessToken}` },
query: { limit: 100 },
});
if (documentsResponse.data) {
setDocuments(documentsResponse.data.documents);
}
// Fetch tools
const toolsResponse = await listToolsApiV1ToolsGet({
headers: { Authorization: `Bearer ${accessToken}` },
});
if (toolsResponse.data) {
setTools(toolsResponse.data);
}
} catch (error) {
console.error('Failed to fetch documents and tools:', error);
}
};
fetchData();
}, [getAccessToken]);
// Memoize defaultEdgeOptions to prevent unnecessary re-renders
const defaultEdgeOptions = useMemo(() => ({
animated: true,
@ -102,7 +136,11 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
}), []);
// Memoize the context value to prevent unnecessary re-renders
const workflowContextValue = useMemo(() => ({ saveWorkflow }), [saveWorkflow]);
const workflowContextValue = useMemo(() => ({
saveWorkflow,
documents,
tools
}), [saveWorkflow, documents, tools]);
return (
<WorkflowProvider value={workflowContextValue}>

View file

@ -1,7 +1,11 @@
import { createContext, useContext } from 'react';
import type { DocumentResponseSchema, ToolResponse } from '@/client/types.gen';
interface WorkflowContextType {
saveWorkflow: (updateWorkflowDefinition?: boolean) => Promise<void>;
documents?: DocumentResponseSchema[];
tools?: ToolResponse[];
}
const WorkflowContext = createContext<WorkflowContextType | undefined>(undefined);
@ -15,3 +19,8 @@ export const useWorkflow = () => {
}
return context;
};
// Optional hook that doesn't throw if context is not available
export const useWorkflowOptional = () => {
return useContext(WorkflowContext);
};

View file

@ -1,9 +1,8 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig } from '@hey-api/client-fetch';
import { createClientConfig } from '../lib/apiClient';
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
import { createClientConfig } from '../lib/apiClient';
/**
* The `createClientConfig()` function will be called on client initialization
@ -17,4 +16,4 @@ export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
baseUrl: 'http://127.0.0.1:8000'
})));
})));

View file

@ -1,3 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './sdk.gen';
export * from './types.gen';
export * from './sdk.gen';

File diff suppressed because one or more lines are too long

View file

@ -351,6 +351,11 @@ export type DefaultConfigurationsResponse = {
[key: string]: unknown;
};
};
embeddings: {
[key: string]: {
[key: string]: unknown;
};
};
default_providers: {
[key: string]: string;
};
@ -672,6 +677,10 @@ export type ProcessDocumentRequestSchema = {
* S3 key of the uploaded file
*/
s3_key: string;
/**
* Embedding service to use for processing. Options: 'openai' (default, 1536-dim, requires API key) or 'sentence_transformer' (free, 384-dim)
*/
embedding_service?: 'sentence_transformer' | 'openai';
};
export type S3SignedUrlResponse = {
@ -924,6 +933,9 @@ export type UserConfigurationRequestResponseSchema = {
stt?: {
[key: string]: string | number;
} | null;
embeddings?: {
[key: string]: string | number;
} | null;
test_phone_number?: string | null;
timezone?: string | null;
organization_pricing?: {
@ -4493,4 +4505,4 @@ export type HealthApiV1HealthGetResponses = {
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
};
};

View file

@ -14,7 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { VoiceSelector } from "@/components/VoiceSelector";
import { useUserConfig } from "@/context/UserConfigContext";
type ServiceSegment = "llm" | "tts" | "stt";
type ServiceSegment = "llm" | "tts" | "stt" | "embeddings";
interface SchemaProperty {
type?: string;
@ -41,6 +41,7 @@ const TAB_CONFIG: { key: ServiceSegment; label: string }[] = [
{ key: "llm", label: "LLM" },
{ key: "tts", label: "Voice" },
{ key: "stt", label: "Transcriber" },
{ key: "embeddings", label: "Embedding" },
];
// Display names for language codes (Deepgram + Sarvam)
@ -109,12 +110,14 @@ export default function ServiceConfiguration() {
const [schemas, setSchemas] = useState<Record<ServiceSegment, Record<string, ProviderSchema>>>({
llm: {},
tts: {},
stt: {}
stt: {},
embeddings: {}
});
const [serviceProviders, setServiceProviders] = useState<Record<ServiceSegment, string>>({
llm: "",
tts: "",
stt: ""
stt: "",
embeddings: ""
});
const [isManualModelInput, setIsManualModelInput] = useState(false);
const [hasCheckedManualMode, setHasCheckedManualMode] = useState(false);
@ -136,7 +139,8 @@ export default function ServiceConfiguration() {
setSchemas({
llm: response.data.llm as Record<string, ProviderSchema>,
tts: response.data.tts as Record<string, ProviderSchema>,
stt: response.data.stt as Record<string, ProviderSchema>
stt: response.data.stt as Record<string, ProviderSchema>,
embeddings: response.data.embeddings as Record<string, ProviderSchema>
});
} else {
console.error("Failed to fetch configurations");
@ -147,7 +151,8 @@ export default function ServiceConfiguration() {
const selectedProviders: Record<ServiceSegment, string> = {
llm: response.data.default_providers.llm,
tts: response.data.default_providers.tts,
stt: response.data.default_providers.stt
stt: response.data.default_providers.stt,
embeddings: response.data.default_providers.embeddings
};
const setServicePropertyValues = (service: ServiceSegment) => {
@ -173,6 +178,7 @@ export default function ServiceConfiguration() {
setServicePropertyValues("llm");
setServicePropertyValues("tts");
setServicePropertyValues("stt");
setServicePropertyValues("embeddings");
// IMPORTANT: Reset form values BEFORE changing providers
// Otherwise, Radix Select sees old values that don't match new provider's enum
@ -246,7 +252,7 @@ export default function ServiceConfiguration() {
setApiError(null);
setIsSaving(true);
const userConfig = {
const userConfig: Record<ServiceSegment, Record<string, string | number>> = {
llm: {
provider: serviceProviders.llm,
api_key: data.llm_api_key as string,
@ -259,6 +265,11 @@ export default function ServiceConfiguration() {
stt: {
provider: serviceProviders.stt,
api_key: data.stt_api_key as string
},
embeddings: {
provider: serviceProviders.embeddings,
api_key: data.embeddings_api_key as string,
model: data.embeddings_model as string
}
};
@ -273,12 +284,25 @@ export default function ServiceConfiguration() {
}
});
// Build save config - only include embeddings if api_key is provided
const saveConfig: {
llm: Record<string, string | number>;
tts: Record<string, string | number>;
stt: Record<string, string | number>;
embeddings?: Record<string, string | number>;
} = {
llm: userConfig.llm,
tts: userConfig.tts,
stt: userConfig.stt
};
// Only include embeddings if user has configured it (has api_key)
if (userConfig.embeddings.api_key) {
saveConfig.embeddings = userConfig.embeddings;
}
try {
await saveUserConfig({
llm: userConfig.llm,
tts: userConfig.tts,
stt: userConfig.stt
});
await saveUserConfig(saveConfig);
setApiError(null);
} catch (error: unknown) {
if (error instanceof Error) {
@ -543,7 +567,7 @@ export default function ServiceConfiguration() {
<Card>
<CardContent className="pt-6">
<Tabs defaultValue="llm" className="w-full">
<TabsList className="grid w-full grid-cols-3 mb-6">
<TabsList className="grid w-full grid-cols-4 mb-6">
{TAB_CONFIG.map(({ key, label }) => (
<TabsTrigger key={key} value={key}>
{label}

View file

@ -1,57 +1,55 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { listDocumentsApiV1KnowledgeBaseDocumentsGet } from "@/client/sdk.gen";
import { useAuth } from "@/lib/auth";
import { useCallback, useEffect, useState } from "react";
import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext";
import type { DocumentResponseSchema } from "@/client/types.gen";
import { Badge } from "@/components/ui/badge";
interface DocumentBadgesProps {
documentUuids: string[];
onStaleUuidsDetected?: (staleUuids: string[]) => void;
}
export const DocumentBadges = ({ documentUuids }: DocumentBadgesProps) => {
const { getAccessToken } = useAuth();
export const DocumentBadges = ({ documentUuids, onStaleUuidsDetected }: DocumentBadgesProps) => {
const { documents } = useWorkflow();
const [documentNames, setDocumentNames] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const fetchDocuments = useCallback(async () => {
if (documentUuids.length === 0) return;
const processDocuments = useCallback((docs: DocumentResponseSchema[]) => {
const nameMap: Record<string, string> = {};
const validUuids = new Set<string>();
setLoading(true);
try {
const accessToken = await getAccessToken();
const response = await listDocumentsApiV1KnowledgeBaseDocumentsGet({
headers: { Authorization: `Bearer ${accessToken}` },
query: {
limit: 100,
},
docs
.filter((doc) => documentUuids.includes(doc.document_uuid))
.forEach((doc) => {
nameMap[doc.document_uuid] = doc.filename;
validUuids.add(doc.document_uuid);
});
setDocumentNames(nameMap);
if (response.data) {
const nameMap: Record<string, string> = {};
response.data.documents
.filter((doc) => documentUuids.includes(doc.document_uuid))
.forEach((doc) => {
nameMap[doc.document_uuid] = doc.filename;
});
setDocumentNames(nameMap);
// Detect stale UUIDs - this only runs when we have loaded data (not undefined)
if (onStaleUuidsDetected) {
const staleUuids = documentUuids.filter(uuid => !validUuids.has(uuid));
if (staleUuids.length > 0) {
onStaleUuidsDetected(staleUuids);
}
} catch (error) {
console.error("Failed to fetch documents:", error);
} finally {
setLoading(false);
}
}, [documentUuids, getAccessToken]);
}, [documentUuids, onStaleUuidsDetected]);
useEffect(() => {
fetchDocuments();
}, [fetchDocuments]);
if (documentUuids.length > 0 && documents !== undefined) {
processDocuments(documents);
} else if (documentUuids.length === 0) {
setDocumentNames({});
}
}, [documentUuids, documents, processDocuments]);
if (documentUuids.length === 0) {
return <></>;
}
if (loading) {
// Show loading while data hasn't loaded yet
if (documents === undefined) {
return <Badge variant="outline">Loading...</Badge>;
}

View file

@ -1,18 +1,18 @@
"use client";
import { FileText } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type { DocumentResponseSchema } from "@/client/types.gen";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { listDocumentsApiV1KnowledgeBaseDocumentsGet } from "@/client/sdk.gen";
import type { DocumentResponseSchema } from "@/client/types.gen";
import { useAuth } from "@/lib/auth";
import { FileText } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
interface DocumentSelectorProps {
value: string[];
onChange: (uuids: string[]) => void;
documents: DocumentResponseSchema[];
disabled?: boolean;
label?: string;
description?: string;
@ -22,43 +22,17 @@ interface DocumentSelectorProps {
export const DocumentSelector = ({
value,
onChange,
documents,
disabled = false,
label = "Knowledge Base Documents",
description = "Select documents that the agent can reference during conversations.",
showLabel = true,
}: DocumentSelectorProps) => {
const { getAccessToken } = useAuth();
const [documents, setDocuments] = useState<DocumentResponseSchema[]>([]);
const [loading, setLoading] = useState(false);
const fetchDocuments = useCallback(async () => {
setLoading(true);
try {
const accessToken = await getAccessToken();
const response = await listDocumentsApiV1KnowledgeBaseDocumentsGet({
headers: { Authorization: `Bearer ${accessToken}` },
query: {
limit: 100,
},
});
if (response.data) {
// Only show completed documents
const completedDocs = response.data.documents.filter(
(doc) => doc.processing_status === "completed"
);
setDocuments(completedDocs);
}
} catch (error) {
console.error("Failed to fetch documents:", error);
} finally {
setLoading(false);
}
}, [getAccessToken]);
useEffect(() => {
fetchDocuments();
}, [fetchDocuments]);
// Only show completed documents
const completedDocuments = useMemo(
() => documents.filter((doc) => doc.processing_status === "completed"),
[documents]
);
const handleToggle = (documentUuid: string, checked: boolean) => {
if (checked) {
@ -76,25 +50,7 @@ export const DocumentSelector = ({
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
};
if (loading) {
return (
<div className="space-y-2">
{showLabel && (
<>
<Label>{label}</Label>
{description && (
<Label className="text-xs text-muted-foreground">{description}</Label>
)}
</>
)}
<div className="border rounded-md p-4 text-sm text-muted-foreground text-center">
Loading documents...
</div>
</div>
);
}
if (documents.length === 0) {
if (completedDocuments.length === 0) {
return (
<div className="space-y-2">
{showLabel && (
@ -133,7 +89,7 @@ export const DocumentSelector = ({
)}
<div className="border rounded-md max-h-[300px] overflow-y-auto">
<div className="divide-y">
{documents.map((doc) => (
{completedDocuments.map((doc) => (
<div
key={doc.document_uuid}
className="flex items-start gap-3 p-3 hover:bg-muted/50 transition-colors"

View file

@ -2,43 +2,43 @@
import { useCallback, useEffect, useState } from "react";
import { listToolsApiV1ToolsGet } from "@/client/sdk.gen";
import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext";
import type { ToolResponse } from "@/client/types.gen";
import { Badge } from "@/components/ui/badge";
import { useAuth } from "@/lib/auth";
interface ToolBadgesProps {
toolUuids: string[];
onStaleUuidsDetected?: (staleUuids: string[]) => void;
}
export function ToolBadges({ toolUuids }: ToolBadgesProps) {
const { getAccessToken } = useAuth();
const [tools, setTools] = useState<ToolResponse[]>([]);
export function ToolBadges({ toolUuids, onStaleUuidsDetected }: ToolBadgesProps) {
const { tools } = useWorkflow();
const [selectedTools, setSelectedTools] = useState<ToolResponse[]>([]);
const fetchTools = useCallback(async () => {
try {
const accessToken = await getAccessToken();
const response = await listToolsApiV1ToolsGet({
headers: { Authorization: `Bearer ${accessToken}` },
});
if (response.data) {
setTools(response.data);
const processTools = useCallback((toolsData: ToolResponse[]) => {
const filtered = toolsData.filter(tool => toolUuids.includes(tool.tool_uuid));
setSelectedTools(filtered);
// Detect stale UUIDs - this only runs when we have loaded data (not undefined)
if (onStaleUuidsDetected) {
const validUuids = new Set(toolsData.map(tool => tool.tool_uuid));
const staleUuids = toolUuids.filter(uuid => !validUuids.has(uuid));
if (staleUuids.length > 0) {
onStaleUuidsDetected(staleUuids);
}
} catch (error) {
console.error("Failed to fetch tools:", error);
}
}, [getAccessToken]);
}, [toolUuids, onStaleUuidsDetected]);
useEffect(() => {
if (toolUuids.length > 0) {
fetchTools();
if (toolUuids.length > 0 && tools !== undefined) {
processTools(tools);
} else if (toolUuids.length === 0) {
setSelectedTools([]);
}
}, [toolUuids.length, fetchTools]);
}, [toolUuids, tools, processTools]);
const selectedTools = tools.filter((tool) => toolUuids.includes(tool.tool_uuid));
if (selectedTools.length === 0 && toolUuids.length > 0) {
// Still loading or tools not found
// Show loading while data hasn't loaded yet
if (tools === undefined && toolUuids.length > 0) {
return (
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="text-xs">

View file

@ -1,20 +1,18 @@
"use client";
import { ExternalLink, Loader2 } from "lucide-react";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { renderToolIcon } from "@/app/tools/config";
import { listToolsApiV1ToolsGet } from "@/client/sdk.gen";
import type { ToolResponse } from "@/client/types.gen";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/lib/auth";
interface ToolSelectorProps {
value: string[];
onChange: (uuids: string[]) => void;
tools: ToolResponse[];
disabled?: boolean;
label?: string;
description?: string;
@ -24,43 +22,14 @@ interface ToolSelectorProps {
export function ToolSelector({
value,
onChange,
tools,
disabled = false,
label = "Tools",
description = "Select tools that the agent can use during the conversation.",
showLabel = true,
}: ToolSelectorProps) {
const { getAccessToken } = useAuth();
const [tools, setTools] = useState<ToolResponse[]>([]);
const [loading, setLoading] = useState(false);
const fetchTools = useCallback(async () => {
setLoading(true);
try {
const accessToken = await getAccessToken();
const response = await listToolsApiV1ToolsGet({
headers: { Authorization: `Bearer ${accessToken}` },
query: { status: "active" },
});
if (response.error) {
console.error("Failed to fetch tools:", response.error);
setTools([]);
return;
}
if (response.data) {
setTools(response.data);
}
} catch (error) {
console.error("Failed to fetch tools:", error);
setTools([]);
} finally {
setLoading(false);
}
}, [getAccessToken]);
useEffect(() => {
fetchTools();
}, [fetchTools]);
// Filter to only show active tools
const activeTools = tools.filter((tool) => tool.status === "active");
const handleToggle = (toolUuid: string, checked: boolean) => {
if (checked) {
@ -83,12 +52,7 @@ export function ToolSelector({
</>
)}
{loading ? (
<div className="flex items-center gap-2 p-3 border rounded-md">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm text-muted-foreground">Loading tools...</span>
</div>
) : tools.length === 0 ? (
{activeTools.length === 0 ? (
<div className="p-4 border rounded-md text-center">
<p className="text-sm text-muted-foreground mb-2">
No tools available.
@ -102,7 +66,7 @@ export function ToolSelector({
</div>
) : (
<div className="border rounded-md divide-y">
{tools.map((tool) => {
{activeTools.map((tool) => {
const isSelected = value.includes(tool.tool_uuid);
return (
<label

View file

@ -1,8 +1,9 @@
import { NodeProps, NodeToolbar, Position } from "@xyflow/react";
import { Edit, FileText, Headset, PlusIcon, Trash2Icon, Wrench } from "lucide-react";
import { memo, useEffect, useMemo, useState } from "react";
import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext";
import type { DocumentResponseSchema, ToolResponse } from "@/client/types.gen";
import { DocumentBadges } from "@/components/flow/DocumentBadges";
import { DocumentSelector } from "@/components/flow/DocumentSelector";
import { ToolBadges } from "@/components/flow/ToolBadges";
@ -38,6 +39,8 @@ interface AgentNodeEditFormProps {
setToolUuids: (value: string[]) => void;
documentUuids: string[];
setDocumentUuids: (value: string[]) => void;
tools: ToolResponse[];
documents: DocumentResponseSchema[];
}
interface AgentNodeProps extends NodeProps {
@ -46,7 +49,7 @@ interface AgentNodeProps extends NodeProps {
export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({ id });
const { saveWorkflow } = useWorkflow();
const { saveWorkflow, tools, documents } = useWorkflow();
// Form state
const [prompt, setPrompt] = useState(data.prompt);
@ -120,6 +123,30 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
}
}, [data, open]);
// Handle cleanup of stale document UUIDs
const handleStaleDocuments = useCallback((staleUuids: string[]) => {
const cleanedUuids = (data.document_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
handleSaveNodeData({
...data,
document_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
});
setTimeout(async () => {
await saveWorkflow();
}, 100);
}, [data, handleSaveNodeData, saveWorkflow]);
// Handle cleanup of stale tool UUIDs
const handleStaleTools = useCallback((staleUuids: string[]) => {
const cleanedUuids = (data.tool_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
handleSaveNodeData({
...data,
tool_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
});
setTimeout(async () => {
await saveWorkflow();
}, 100);
}, [data, handleSaveNodeData, saveWorkflow]);
return (
<>
<NodeContent
@ -144,7 +171,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
<Wrench className="h-3 w-3" />
<span>Tools:</span>
</div>
<ToolBadges toolUuids={data.tool_uuids} />
<ToolBadges toolUuids={data.tool_uuids} onStaleUuidsDetected={handleStaleTools} />
</div>
)}
{data.document_uuids && data.document_uuids.length > 0 && (
@ -153,7 +180,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
<FileText className="h-3 w-3" />
<span>Documents:</span>
</div>
<DocumentBadges documentUuids={data.document_uuids} />
<DocumentBadges documentUuids={data.document_uuids} onStaleUuidsDetected={handleStaleDocuments} />
</div>
)}
</NodeContent>
@ -198,6 +225,8 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
setToolUuids={setToolUuids}
documentUuids={documentUuids}
setDocumentUuids={setDocumentUuids}
tools={tools ?? []}
documents={documents ?? []}
/>
)}
</NodeEditDialog>
@ -224,6 +253,8 @@ const AgentNodeEditForm = ({
setToolUuids,
documentUuids,
setDocumentUuids,
tools,
documents,
}: AgentNodeEditFormProps) => {
const handleVariableNameChange = (idx: number, value: string) => {
const newVars = [...variables];
@ -364,6 +395,7 @@ const AgentNodeEditForm = ({
<ToolSelector
value={toolUuids}
onChange={setToolUuids}
tools={tools}
description="Select tools that the agent can invoke during this conversation step."
/>
</div>
@ -373,6 +405,7 @@ const AgentNodeEditForm = ({
<DocumentSelector
value={documentUuids}
onChange={setDocumentUuids}
documents={documents}
description="Select documents from the knowledge base that the agent can reference during this conversation step."
/>
</div>

View file

@ -1,8 +1,9 @@
import { NodeProps, NodeToolbar, Position } from "@xyflow/react";
import { Edit, FileText, Play, PlusIcon, Trash2Icon, Wrench } from "lucide-react";
import { memo, useEffect, useMemo, useState } from "react";
import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext";
import type { DocumentResponseSchema, ToolResponse } from "@/client/types.gen";
import { DocumentBadges } from "@/components/flow/DocumentBadges";
import { DocumentSelector } from "@/components/flow/DocumentSelector";
import { ToolBadges } from "@/components/flow/ToolBadges";
@ -45,6 +46,8 @@ interface StartCallEditFormProps {
setToolUuids: (value: string[]) => void;
documentUuids: string[];
setDocumentUuids: (value: string[]) => void;
tools: ToolResponse[];
documents: DocumentResponseSchema[];
}
interface StartCallNodeProps extends NodeProps {
@ -56,7 +59,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
id,
additionalData: { is_start: true }
});
const { saveWorkflow } = useWorkflow();
const { saveWorkflow, tools, documents } = useWorkflow();
// Form state
const [prompt, setPrompt] = useState(data.prompt ?? "");
@ -140,6 +143,30 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
}
}, [data, open]);
// Handle cleanup of stale document UUIDs
const handleStaleDocuments = useCallback((staleUuids: string[]) => {
const cleanedUuids = (data.document_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
handleSaveNodeData({
...data,
document_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
});
setTimeout(async () => {
await saveWorkflow();
}, 100);
}, [data, handleSaveNodeData, saveWorkflow]);
// Handle cleanup of stale tool UUIDs
const handleStaleTools = useCallback((staleUuids: string[]) => {
const cleanedUuids = (data.tool_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
handleSaveNodeData({
...data,
tool_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
});
setTimeout(async () => {
await saveWorkflow();
}, 100);
}, [data, handleSaveNodeData, saveWorkflow]);
return (
<>
<NodeContent
@ -163,7 +190,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
<Wrench className="h-3 w-3" />
<span>Tools:</span>
</div>
<ToolBadges toolUuids={data.tool_uuids} />
<ToolBadges toolUuids={data.tool_uuids} onStaleUuidsDetected={handleStaleTools} />
</div>
)}
{data.document_uuids && data.document_uuids.length > 0 && (
@ -172,7 +199,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
<FileText className="h-3 w-3" />
<span>Documents:</span>
</div>
<DocumentBadges documentUuids={data.document_uuids} />
<DocumentBadges documentUuids={data.document_uuids} onStaleUuidsDetected={handleStaleDocuments} />
</div>
)}
</NodeContent>
@ -218,6 +245,8 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
setToolUuids={setToolUuids}
documentUuids={documentUuids}
setDocumentUuids={setDocumentUuids}
tools={tools ?? []}
documents={documents ?? []}
/>
)}
</NodeEditDialog>
@ -250,6 +279,8 @@ const StartCallEditForm = ({
setToolUuids,
documentUuids,
setDocumentUuids,
tools,
documents,
}: StartCallEditFormProps) => {
const handleVariableNameChange = (idx: number, value: string) => {
const newVars = [...variables];
@ -435,6 +466,7 @@ const StartCallEditForm = ({
<ToolSelector
value={toolUuids}
onChange={setToolUuids}
tools={tools}
description="Select tools that the agent can invoke during this conversation step."
/>
</div>
@ -444,6 +476,7 @@ const StartCallEditForm = ({
<DocumentSelector
value={documentUuids}
onChange={setDocumentUuids}
documents={documents}
description="Select documents from the knowledge base that the agent can reference during this conversation step."
/>
</div>

View file

@ -18,6 +18,9 @@ export type SaveUserConfigFunctionParams = {
stt?: {
[key: string]: string | number;
} | null;
embeddings?: {
[key: string]: string | number;
} | null;
test_phone_number?: string | null;
timezone?: string | null;
};