mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
feat: add openai embedding service
This commit is contained in:
parent
eb41285204
commit
3f0e500fde
39 changed files with 1902 additions and 339 deletions
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
})));
|
||||
})));
|
||||
|
|
@ -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
|
|
@ -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 & {});
|
||||
};
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue