Prompt LLM works now

This commit is contained in:
Laurie Voss
2025-07-17 15:07:45 -07:00
parent c244c6c459
commit 460d6b80d4
8 changed files with 187 additions and 88 deletions
+2 -28
View File
@@ -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[] = [];
+24
View File
@@ -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 });
}
}
+9
View File
@@ -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 (
<Card className="p-3 space-y-4">
{/* LlamaCloud API Key */}
+2 -1
View File
@@ -210,7 +210,7 @@ const AgentFlowInner = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes,
<AgentBuilderSidebar onAddNode={onAddNode} onReset={onReset} settings={settings} onUpdateSettings={onUpdateSettings} />
<div className="flex-1" ref={reactFlowWrapper}>
<div className="flex-1 h-full">
<ReactFlow
nodes={nodes}
edges={edges}
@@ -222,6 +222,7 @@ const AgentFlowInner = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes,
nodeTypes={nodeTypes}
deleteKeyCode={['Backspace', 'Delete']}
fitView
proOptions={{ hideAttribution: true }}
className="bg-flow-bg"
defaultEdgeOptions={{
type: 'default',
+70 -6
View File
@@ -74,6 +74,28 @@ const loadSavedGraph = () => {
};
};
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<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
@@ -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',
+2 -48
View File
@@ -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 (
<div className={`agent-node node-llm ${selected ? 'selected' : ''}`}>
<div className="node-content flex flex-col items-center justify-center text-foreground p-4 min-w-[180px]">
<Brain className="w-5 h-5 mb-2" />
<span className="text-sm font-medium mb-2">{data.label || 'Prompt LLM'}</span>
<div className="w-full space-y-1">
<input
type="text"
value={data.model || ''}
placeholder="Model (e.g., gpt-4)"
className="w-full px-2 py-1 text-xs bg-muted border border-border rounded text-foreground placeholder-muted-foreground"
onChange={(e) => handleModelChange(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
<input
type="number"
value={data.temperature !== undefined ? data.temperature.toString() : ''}
placeholder="Temperature"
min="0"
max="2"
step="0.1"
className="w-full px-2 py-1 text-xs bg-muted border border-border rounded text-foreground placeholder-muted-foreground"
onChange={(e) => handleTemperatureChange(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
<Handle type="target" position={Position.Top} />
<Handle type="source" position={Position.Bottom} />
@@ -71,4 +25,4 @@ const PromptLLMNode = memo(({ id, data, selected }: PromptLLMNodeProps) => {
PromptLLMNode.displayName = 'PromptLLMNode';
export default PromptLLMNode;
export default PromptLLMNode;
+39
View File
@@ -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;
}
+39 -5
View File
@@ -29,14 +29,17 @@ const getEventName = (eventId: string) => eventId.replace(/-/g, "_");
const generateImports = (json: WorkflowJson): string => {
const imports = new Set<string>();
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(