diff --git a/app/api/agent/run/route.ts b/app/api/agent/run/route.ts index ec0555c..086cdd8 100644 --- a/app/api/agent/run/route.ts +++ b/app/api/agent/run/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; -import { OpenAI } from '@llamaindex/openai'; -import { Anthropic } from '@llamaindex/anthropic'; -import { Gemini, GEMINI_MODEL } from '@llamaindex/google'; import { Settings, LLM, tool } from 'llamaindex'; import { agent } from '@llamaindex/workflow'; import { z } from 'zod'; +import { getLlm } from '@/lib/llm-utils'; export async function POST(req: NextRequest) { try { @@ -50,31 +48,7 @@ export async function POST(req: NextRequest) { } // 2. Configure LLM - const llmProvider = settings?.defaultLLM || 'gpt-4-turbo'; - let llm; - let llmApiKey: string | undefined; - - if (llmProvider.startsWith('gpt')) { - llmApiKey = settings?.apiKeys?.openai; - llm = new OpenAI({ model: 'gpt-4.1-mini', apiKey: llmApiKey }); - } else if (llmProvider.startsWith('claude')) { - llmApiKey = settings?.apiKeys?.anthropic; - llm = new Anthropic({ - model: 'claude-sonnet-4-20250514', - apiKey: llmApiKey, - }); - } else if (llmProvider.startsWith('gemini')) { - llmApiKey = settings?.apiKeys?.google; - llm = new Gemini({ - model: GEMINI_MODEL.GEMINI_PRO_LATEST, - apiKey: llmApiKey, - }); - } else { - return NextResponse.json( - { error: `Unsupported LLM provider: ${llmProvider}` }, - { status: 400 }, - ); - } + const llm = getLlm(settings); // 3. Create dynamic tools for the agent const tools: any[] = []; diff --git a/app/api/llm/call/route.ts b/app/api/llm/call/route.ts new file mode 100644 index 0000000..63edd43 --- /dev/null +++ b/app/api/llm/call/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getLlm } from "@/lib/llm-utils"; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { input, node, settings } = body; + + if (!input || !node || !settings) { + return NextResponse.json({ error: "Missing required parameters" }, { status: 400 }); + } + + const llm = getLlm(settings, node); + + const response = await llm.chat({ + messages: [{ role: "user", content: typeof input === 'string' ? input : JSON.stringify(input) }] + }); + + return NextResponse.json({ output: response.message.content }); + + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/components/AgentBuilderSettings.tsx b/src/components/AgentBuilderSettings.tsx index 772760c..1690782 100644 --- a/src/components/AgentBuilderSettings.tsx +++ b/src/components/AgentBuilderSettings.tsx @@ -31,6 +31,7 @@ interface AgentBuilderSettingsProps { } const AgentBuilderSettings = memo(({ settings, onUpdateSettings }: AgentBuilderSettingsProps) => { + const [isClient, setIsClient] = useState(false); const [showApiKeys, setShowApiKeys] = useState({ llamaCloud: false, openai: false, @@ -62,6 +63,10 @@ const AgentBuilderSettings = memo(({ settings, onUpdateSettings }: AgentBuilderS })); }; + useEffect(() => { + setIsClient(true); + }, []); + // Get the current API key based on selected LLM const getCurrentApiKey = () => { switch (settings.defaultLLM) { @@ -119,6 +124,10 @@ const AgentBuilderSettings = memo(({ settings, onUpdateSettings }: AgentBuilderS } }; + if (!isClient) { + return null; + } + return ( {/* LlamaCloud API Key */} diff --git a/src/components/AgentFlow.tsx b/src/components/AgentFlow.tsx index 6a700ab..5823bf6 100644 --- a/src/components/AgentFlow.tsx +++ b/src/components/AgentFlow.tsx @@ -210,7 +210,7 @@ const AgentFlowInner = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes, -
+
{ }; }; +const getMessageContent = (data: any): string => { + if (typeof data === 'string') { + return data; + } + if (Array.isArray(data) && data.length > 0 && data[0].type === 'text' && typeof data[0].text === 'string') { + return data[0].text; + } + return JSON.stringify(data, null, 2); +} + +const loadSavedSettings = () => { + try { + const savedSettings = localStorage.getItem('agent-builder-settings'); + if (savedSettings) { + return JSON.parse(savedSettings); + } + } catch (error) { + console.error('Error loading saved settings:', error); + } + return {}; +} + const RunViewInner = () => { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -158,21 +180,61 @@ const RunViewInner = () => { // The workflow will now wait for user input. // The onUserInput function will be called when the user submits a message. return; // Don't proceed further until user input - case 'promptLLM': - // This will be handled similarly to promptAgent - const llmOutput = `Mock output from ${node.data.label}`; + case 'promptLLM': { + // find the previous node to get the input + const incomingEdge = edges.find((e) => e.target === nodeId); + const parentNodeId = incomingEdge?.source; + const input = parentNodeId ? workflowStateRef.current[parentNodeId] : null; + + if (!input) { + setError(`Input for node ${nodeId} not found.`); + setExecutionStatus('error'); + return; + } + + const thinkingMessageId = Math.random().toString(); setMessages((prev) => [ ...prev, { - id: Math.random().toString(), + id: thinkingMessageId, role: 'assistant', - content: `Executing ${node.data.label}...`, + content: `Thinking with ${node.data.label}...`, }, ]); + + const llmResponse = await fetch('/api/llm/call', { + method: 'POST', + body: JSON.stringify({ + input: input, + node: node, + settings: loadSavedSettings() + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!llmResponse.ok) { + throw new Error( + `LLM execution failed: ${await llmResponse.text()}`, + ); + } + + const { output: llmOutput } = await llmResponse.json(); + setWorkflowState((prevState) => ({ ...prevState, [nodeId]: llmOutput, })); + + setMessages((prev) => + prev.map((msg) => + msg.id === thinkingMessageId + ? { ...msg, content: getMessageContent(llmOutput) } + : msg + ) + ); + const llmEdge = edges.find((e) => e.source === nodeId); if (llmEdge) { nextNodeId = llmEdge.target; @@ -180,6 +242,7 @@ const RunViewInner = () => { setExecutionStatus('finished'); } break; + } case 'promptAgent': const compiledWorkflow = compileWorkflow(nodes, edges); const response = await fetch('/api/agent/run', { @@ -207,7 +270,7 @@ const RunViewInner = () => { { id: Math.random().toString(), role: 'assistant', - content: output, + content: getMessageContent(output), }, ]); @@ -305,6 +368,7 @@ const RunViewInner = () => { nodesDraggable={false} nodesConnectable={false} elementsSelectable={false} + proOptions={{ hideAttribution: true }} className="bg-flow-bg" defaultEdgeOptions={{ type: 'default', diff --git a/src/components/nodes/PromptLLMNode.tsx b/src/components/nodes/PromptLLMNode.tsx index 27e25ef..374a604 100644 --- a/src/components/nodes/PromptLLMNode.tsx +++ b/src/components/nodes/PromptLLMNode.tsx @@ -1,67 +1,21 @@ import { memo } from 'react'; -import { Handle, Position, useReactFlow } from '@xyflow/react'; +import { Handle, Position } from '@xyflow/react'; import { Brain } from 'lucide-react'; interface PromptLLMNodeProps { id: string; data: { label?: string; - model?: string; - temperature?: number; }; selected?: boolean; } const PromptLLMNode = memo(({ id, data, selected }: PromptLLMNodeProps) => { - const { setNodes } = useReactFlow(); - - const handleModelChange = (value: string) => { - setNodes((nodes) => - nodes.map((node) => - node.id === id - ? { ...node, data: { ...node.data, model: value } } - : node - ) - ); - }; - - const handleTemperatureChange = (value: string) => { - const temperature = value === '' ? undefined : parseFloat(value); - setNodes((nodes) => - nodes.map((node) => - node.id === id - ? { ...node, data: { ...node.data, temperature } } - : node - ) - ); - }; - return (
{data.label || 'Prompt LLM'} -
- handleModelChange(e.target.value)} - onClick={(e) => e.stopPropagation()} - /> - handleTemperatureChange(e.target.value)} - onClick={(e) => e.stopPropagation()} - /> -
@@ -71,4 +25,4 @@ const PromptLLMNode = memo(({ id, data, selected }: PromptLLMNodeProps) => { PromptLLMNode.displayName = 'PromptLLMNode'; -export default PromptLLMNode; \ No newline at end of file +export default PromptLLMNode; diff --git a/src/lib/llm-utils.ts b/src/lib/llm-utils.ts new file mode 100644 index 0000000..0c851f1 --- /dev/null +++ b/src/lib/llm-utils.ts @@ -0,0 +1,39 @@ +import { OpenAI } from "@llamaindex/openai"; +import { Anthropic } from "@llamaindex/anthropic"; +import { Gemini, GEMINI_MODEL } from "@llamaindex/google"; +import { LLM } from "llamaindex"; + +export function getLlm(settings: any, nodeData: any = {}): LLM { + const llmProvider = settings?.defaultLLM || "gpt-4o"; + let llm: LLM; + let llmApiKey: string | undefined; + + const model = nodeData?.data?.model; + const temperature = nodeData?.data?.temperature; + + if (llmProvider.startsWith("gpt")) { + llmApiKey = settings?.apiKeys?.openai; + llm = new OpenAI({ + model: model || "gpt-4.1-mini", + temperature: temperature ?? 0.2, + apiKey: llmApiKey + }); + } else if (llmProvider.startsWith("claude")) { + llmApiKey = settings?.apiKeys?.anthropic; + llm = new Anthropic({ + model: model || "claude-sonnet-4-20250514", + temperature: temperature ?? 0.2, + apiKey: llmApiKey, + }); + } else if (llmProvider.startsWith("gemini")) { + llmApiKey = settings?.apiKeys?.google; + llm = new Gemini({ + model: model || "gemini-pro", + temperature: temperature ?? 0.2, + apiKey: llmApiKey, + }); + } else { + throw new Error(`Unsupported LLM provider: ${llmProvider}`); + } + return llm; +} diff --git a/src/lib/typescript-compiler.ts b/src/lib/typescript-compiler.ts index e6f8793..ac0eb0b 100644 --- a/src/lib/typescript-compiler.ts +++ b/src/lib/typescript-compiler.ts @@ -29,14 +29,17 @@ const getEventName = (eventId: string) => eventId.replace(/-/g, "_"); const generateImports = (json: WorkflowJson): string => { const imports = new Set(); const hasPromptAgent = json.nodes.some((node) => node.type === "promptAgent"); + const hasPromptLLM = json.nodes.some((node) => node.type === "promptLLM"); const hasTools = json.nodes.some( (node: any) => node.type === "promptAgent" && node.tools && node.tools.length > 0, ); const hasUserInput = json.nodes.some((node) => node.type === "userInput"); - if (hasPromptAgent) { - imports.add('import { agent } from "@llamaindex/workflow";'); + if (hasPromptAgent || hasPromptLLM) { + if (hasPromptAgent) { + imports.add('import { agent } from "@llamaindex/workflow";'); + } const llmProvider = json.settings?.defaultLLM || "gpt-4o"; if (llmProvider.startsWith("gpt")) { imports.add('import { OpenAI } from "@llamaindex/openai";'); @@ -87,7 +90,7 @@ const generateLlmInit = (json: WorkflowJson): string => { }); } - if (json.nodes.some((node) => node.type === "promptAgent")) { + if (json.nodes.some((node) => node.type === "promptAgent" || node.type === "promptLLM")) { const llmProvider = json.settings?.defaultLLM || "gpt-4o"; if (llmProvider.startsWith("gpt")) { lines.push( @@ -231,8 +234,9 @@ const generateEvents = (nodes: WorkflowNodeJson[]): string => { .join("\n"); }; -const generateHandlers = (nodes: WorkflowNodeJson[]): string => { +const generateHandlers = (json: WorkflowJson): string => { let handlerLines: string[] = []; + const { nodes } = json; for (const node of nodes) { if (node.type === "stop") { @@ -276,6 +280,36 @@ const generateHandlers = (nodes: WorkflowNodeJson[]): string => { (node as any).prompt?.replace(/`/g, "\\`") || "Enter your input:"; handlerBody = ` return ${needInputEventName}.with("${prompt}");`; + } else if (node.type === "promptLLM") { + const nodeData = node as any; + const model = nodeData.data?.model; + const temperature = nodeData.data?.temperature; + const settings = json.settings; + const defaultLLM = settings?.defaultLLM || "gpt-4o"; + + let llmToUse = "llm"; + let llmDefinition = ""; + + if (model || typeof temperature !== "undefined") { + const llmVar = `llm_${node.id.replace(/-/g, "_")}`; + llmToUse = llmVar; + const defaultModelForProvider = defaultLLM.startsWith("gpt") + ? "gpt-4.1-mini" + : defaultLLM.startsWith("claude") + ? "claude-sonnet-4-20250514" + : "gemini-2.5-pro-latest"; + const finalModel = model || defaultModelForProvider; + const tempValue = typeof temperature !== "undefined" ? temperature : 0.2; + + let llmClass = "OpenAI"; + if (defaultLLM.startsWith("claude")) llmClass = "Anthropic"; + else if (defaultLLM.startsWith("gemini")) llmClass = "Gemini"; + llmDefinition = ` const ${llmVar} = new ${llmClass}({ model: "${finalModel}", temperature: ${tempValue} });`; + } + handlerBody = ` +${llmDefinition} + const result = await ${llmToUse}.chat({ messages: [{ role: "user", content: typeof ctx.data === 'string' ? ctx.data : JSON.stringify(ctx.data) }]}); + return ${nextEventName}.with(result.message.content);`; } else if (node.type === "promptAgent") { const toolNames = (node as any).tools?.map( @@ -366,7 +400,7 @@ export const generateTypescript = (json: WorkflowJson): string => { const llmInit = generateLlmInit(json); const tools = generateTools(json); const events = generateEvents(json.nodes); - const handlers = generateHandlers(json.nodes); + const handlers = generateHandlers(json); const execution = generateExecution("startEvent", "stopEvent", json.nodes); return LlamaIndexTemplate(