diff --git a/.husky/pre-commit b/.husky/pre-commit index 51492ffe2..044922ae0 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,6 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npx lint-staged \ No newline at end of file +cd "$(git rev-parse --show-toplevel)" || exit 1 + +npx lint-staged --config package.json diff --git a/archive/deprecated-providers/groq-ai/src/index.ts b/archive/deprecated-providers/groq-ai/src/index.ts index 629614f8a..0b0cb466b 100644 --- a/archive/deprecated-providers/groq-ai/src/index.ts +++ b/archive/deprecated-providers/groq-ai/src/index.ts @@ -19,6 +19,29 @@ import zodToJsonSchema from "zod-to-json-schema"; import type { GroqProviderOptions } from "./types"; import { convertToolsForSDK } from "./utils"; +type GroqUsage = { + promptTokens: number; + completionTokens: number; + totalTokens: number; +}; + +type GroqStreamState = { + accumulatedText: string; + usage?: GroqUsage; + groqMessages: Groq.Chat.ChatCompletionMessageParam[]; +}; + +type GroqStreamConfig = { + model: string; + temperature: number; + maxTokens?: number; + topP?: number; + frequencyPenalty?: number; + presencePenalty?: number; + stopSequences?: string[] | string; + tools?: Groq.Chat.ChatCompletionTool[]; +}; + // Deprecation warning console.warn( "\x1b[33m⚠️ DEPRECATION WARNING: @voltagent/groq-ai is deprecated and will no longer receive updates.\x1b[0m\n" + @@ -208,6 +231,208 @@ export class GroqProvider implements LLMProvider { return null; }; + private buildStreamParams( + config: GroqStreamConfig, + messages: Groq.Chat.ChatCompletionMessageParam[], + includeTools: boolean, + ) { + return { + model: config.model, + messages, + temperature: config.temperature, + max_tokens: config.maxTokens, + top_p: config.topP, + frequency_penalty: config.frequencyPenalty, + presence_penalty: config.presencePenalty, + stop: config.stopSequences, + stream: true, + ...(includeTools ? { tools: config.tools } : {}), + }; + } + + private updateUsageFromChunk( + chunk: Groq.Chat.ChatCompletionChunk, + currentUsage: GroqUsage | undefined, + ): GroqUsage | undefined { + if (chunk.x_groq?.usage) { + return { + promptTokens: chunk.x_groq.usage.prompt_tokens, + completionTokens: chunk.x_groq.usage.completion_tokens, + totalTokens: chunk.x_groq.usage.total_tokens, + }; + } + return currentUsage; + } + + private async emitTextChunk( + content: string, + controller: ReadableStreamDefaultController, + options: StreamTextOptions, + state: GroqStreamState, + ): Promise { + if (!content) { + return; + } + + state.accumulatedText += content; + controller.enqueue(content); + + if (options.onChunk) { + const step = { + id: "", + type: "text" as const, + content, + role: "assistant" as MessageRole, + }; + await options.onChunk(step); + } + } + + private async executeToolCalls( + toolCalls: Array, + tools: NonNullable["tools"]>, + options: StreamTextOptions, + usage: GroqUsage | undefined, + groqMessages: Groq.Chat.ChatCompletionMessageParam[], + ): Promise> { + const toolResults: Array<{ toolCallId: string; name: string; output: any }> = []; + + for (const toolCall of toolCalls) { + const step = this.createStepFromChunk({ + type: "tool-call", + toolCallId: toolCall.id, + toolName: toolCall.function?.name, + args: toolCall.function?.arguments, + usage, + }); + if (step && options.onChunk) await options.onChunk(step); + if (step && options.onStepFinish) await options.onStepFinish(step); + + const functionName = toolCall.function?.name; + const functionToCall = tools.find((toolItem) => functionName === toolItem.name)?.execute; + const functionArgs = JSON.parse( + toolCall.function?.arguments ? toolCall.function?.arguments : "{}", + ); + if (functionToCall === undefined) { + throw new Error(`Function ${functionName} not found in tools`); + } + const functionResponse = await functionToCall(functionArgs); + if (functionResponse === undefined) { + throw new Error(`Function ${functionName} returned undefined`); + } + toolResults.push({ + toolCallId: toolCall.id, + name: functionName, + output: functionResponse, + }); + + groqMessages.push({ + tool_call_id: toolCall.id ? toolCall.id : "", + role: "tool", + content: JSON.stringify(functionResponse), + }); + } + + return toolResults; + } + + private async emitToolResults( + toolResults: Array<{ toolCallId: string; name: string; output: any }>, + options: StreamTextOptions, + usage: GroqUsage | undefined, + ): Promise { + if (toolResults.length === 0) { + return; + } + + for (const toolResult of toolResults) { + const step = this.createStepFromChunk({ + type: "tool-result", + toolCallId: toolResult.toolCallId, + toolName: toolResult.name, + result: toolResult.output, + usage, + }); + if (step && options.onChunk) await options.onChunk(step); + if (step && options.onStepFinish) await options.onStepFinish(step); + } + } + + private async processStreamChunk(params: { + chunk: Groq.Chat.ChatCompletionChunk; + controller: ReadableStreamDefaultController; + options: StreamTextOptions; + state: GroqStreamState; + streamConfig: GroqStreamConfig; + }): Promise { + const { chunk, controller, options, state, streamConfig } = params; + const content = chunk.choices[0]?.delta?.content || ""; + await this.emitTextChunk(content, controller, options, state); + state.usage = this.updateUsageFromChunk(chunk, state.usage); + + const toolCalls = chunk.choices[0]?.delta?.tool_calls || []; + if (toolCalls.length === 0 || !options.tools) { + return; + } + + const toolResults = await this.executeToolCalls( + toolCalls, + options.tools, + options, + state.usage, + state.groqMessages, + ); + await this.emitToolResults(toolResults, options, state.usage); + + const secondStream = await this.groq.chat.completions.create( + this.buildStreamParams(streamConfig, state.groqMessages, false), + ); + + for await (const followUpChunk of secondStream) { + const followUpContent = followUpChunk.choices[0]?.delta?.content || ""; + await this.emitTextChunk(followUpContent, controller, options, state); + state.usage = this.updateUsageFromChunk(followUpChunk, state.usage); + } + } + + private async runTextStreamProcessing(params: { + stream: AsyncIterable; + controller: ReadableStreamDefaultController; + options: StreamTextOptions; + state: GroqStreamState; + streamConfig: GroqStreamConfig; + }): Promise { + const { stream, controller, options, state, streamConfig } = params; + for await (const chunk of stream) { + await this.processStreamChunk({ chunk, controller, options, state, streamConfig }); + } + + controller.close(); + await this.finalizeTextStream(options, state); + } + + private async finalizeTextStream( + options: StreamTextOptions, + state: GroqStreamState, + ): Promise { + if (options.onFinish) { + await options.onFinish({ + text: state.accumulatedText, + }); + } + + if (options.onStepFinish && state.accumulatedText) { + const textStep = { + id: "", + type: "text" as const, + content: state.accumulatedText, + role: "assistant" as MessageRole, + usage: state.usage, + }; + await options.onStepFinish(textStep); + } + } + generateText = async ( options: GenerateTextOptions, ): Promise> => { @@ -372,184 +597,38 @@ export class GroqProvider implements LLMProvider { } = options.provider || {}; // Create stream from Groq API - const stream = await this.groq.chat.completions.create({ + const streamConfig: GroqStreamConfig = { model: options.model, - messages: groqMessages, temperature, - max_tokens: maxTokens, - top_p: topP, - frequency_penalty: frequencyPenalty, - presence_penalty: presencePenalty, - stop: stopSequences, + maxTokens, + topP, + frequencyPenalty, + presencePenalty, + stopSequences, tools: groqTools, - // Enable streaming - stream: true, - }); + }; + const stream = await this.groq.chat.completions.create( + this.buildStreamParams(streamConfig, groqMessages, true), + ); - let accumulatedText = ""; - let usage: { - promptTokens: number; - completionTokens: number; - totalTokens: number; + const state: GroqStreamState = { + accumulatedText: "", + usage: undefined, + groqMessages, }; - const that = this; // Preserve 'this' context for the stream processing + const provider = this; // Preserve 'this' context for the stream processing // Create a readable stream to return to the caller const textStream = createAsyncIterableStream( new ReadableStream({ async start(controller) { try { - // Process each chunk from the Groq stream - for await (const chunk of stream) { - // Extract content from the chunk - const content = chunk.choices[0]?.delta?.content || ""; - // If we have content, add it to accumulated text and emit it - if (content) { - accumulatedText += content; - controller.enqueue(content); - - // Call onChunk with text chunk - if (options.onChunk) { - const step = { - id: "", - type: "text" as const, - content, - role: "assistant" as MessageRole, - }; - await options.onChunk(step); - } - } - - if (chunk.x_groq?.usage) { - usage = { - promptTokens: chunk.x_groq?.usage.prompt_tokens, - completionTokens: chunk.x_groq?.usage.completion_tokens, - totalTokens: chunk.x_groq?.usage.total_tokens, - }; - } - - const toolCalls = chunk.choices[0]?.delta?.tool_calls || []; - const toolResults = []; - - if (toolCalls && toolCalls.length > 0 && options && options.tools) { - for (const toolCall of toolCalls) { - // Handle all tool calls - each as a separate step - const step = that.createStepFromChunk({ - type: "tool-call", - toolCallId: toolCall.id, - toolName: toolCall.function?.name, - args: toolCall.function?.arguments, - usage: usage, - }); - if (step && options.onChunk) await options.onChunk(step); - if (step && options.onStepFinish) await options.onStepFinish(step); - //Call the function with the arguments - const functionName = toolCall.function?.name; - const functionToCall = options.tools.find( - (toolItem) => functionName === toolItem.name, - )?.execute; - const functionArgs = JSON.parse( - toolCall.function?.arguments ? toolCall.function?.arguments : "{}", - ); - if (functionToCall === undefined) { - throw `Function ${functionName} not found in tools`; - } - const functionResponse = await functionToCall(functionArgs); - if (functionResponse === undefined) { - throw `Function ${functionName} returned undefined`; - } - toolResults.push({ - toolCallId: toolCall.id, - name: functionName, - output: functionResponse, - }); - - groqMessages.push({ - tool_call_id: toolCall.id ? toolCall.id : "", - role: "tool", - content: JSON.stringify(functionResponse), - }); - } - // Handle all tool results - each as a separate step - if (toolCalls && toolResults && toolResults.length > 0) { - for (const toolResult of toolResults) { - const step = that.createStepFromChunk({ - type: "tool-result", - toolCallId: toolResult.toolCallId, - toolName: toolResult.name, - result: toolResult.output, - usage: usage, - }); - if (step && options.onChunk) await options.onChunk(step); - if (step && options.onStepFinish) await options.onStepFinish(step); - } - } - // Call Groq API - const secondStream = await that.groq.chat.completions.create({ - model: options.model, - messages: groqMessages, - temperature, - max_tokens: maxTokens, - top_p: topP, - frequency_penalty: frequencyPenalty, - presence_penalty: presencePenalty, - stop: stopSequences, - stream: true, - }); - - for await (const chunk of secondStream) { - // Extract content from the chunk - const content = chunk.choices[0]?.delta?.content || ""; - // If we have content, add it to accumulated text and emit it - if (content) { - accumulatedText += content; - controller.enqueue(content); - - // Call onChunk with text chunk - if (options.onChunk) { - const step = { - id: "", - type: "text" as const, - content, - role: "assistant" as MessageRole, - }; - await options.onChunk(step); - } - } - - if (chunk.x_groq?.usage) { - usage = { - promptTokens: chunk.x_groq?.usage.prompt_tokens, - completionTokens: chunk.x_groq?.usage.completion_tokens, - totalTokens: chunk.x_groq?.usage.total_tokens, - }; - } - } - } - } - - // When stream completes, close the controller - controller.close(); - - // Call onFinish with complete result - if (options.onFinish) { - await options.onFinish({ - text: accumulatedText, - }); - } - - // Call onStepFinish with complete result if provided - if (options.onStepFinish) { - if (accumulatedText) { - const textStep = { - id: "", - type: "text" as const, - content: accumulatedText, - role: "assistant" as MessageRole, - usage, - }; - await options.onStepFinish(textStep); - } - } + await provider.runTextStreamProcessing({ + stream, + controller, + options, + state, + streamConfig, + }); } catch (error) { // Handle errors during streaming console.error("Error during Groq stream processing:", error); diff --git a/examples/with-lancedb/tsconfig.json b/examples/with-lancedb/tsconfig.json index 43e4d2939..6afe170ca 100644 --- a/examples/with-lancedb/tsconfig.json +++ b/examples/with-lancedb/tsconfig.json @@ -10,4 +10,4 @@ }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 003affe12..d35f6213d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "license": "MIT", "lint-staged": { "*.{js,jsx,ts,tsx}": [ - "biome check --apply --no-errors-on-unmatched" + "biome check --write --no-errors-on-unmatched" ], "*.{md,mdx}": [ "prettier --config ./.prettierrc --write" @@ -72,7 +72,7 @@ "lint": "biome check .", "lint:ci": "biome ci .", "lint:error": "biome check . --diagnostic-level error", - "lint:fix": "biome check . --apply", + "lint:fix": "biome check . --write", "lint:staged": "lint-staged", "nuke": "echo 'Removing all node_modules, builds and lockfiles' && pnpm nuke:node_modules && pnpm nuke:builds && pnpm nuke:lockfiles", "nuke:builds": "lerna exec -- rimraf dist && lerna exec -- rimraf build", diff --git a/packages/core/src/agent/subagent/index.ts b/packages/core/src/agent/subagent/index.ts index 92a4453bf..2005b4de2 100644 --- a/packages/core/src/agent/subagent/index.ts +++ b/packages/core/src/agent/subagent/index.ts @@ -391,190 +391,36 @@ ${task}\n\nContext: ${safeStringify(contextObj, { indentation: 2 })}`; if (this.isDirectAgent(targetAgentConfig)) { // Direct agent - use streamText by default - const response = await targetAgent.streamText(messages, baseOptions); - const forwardingMetadata = this.buildForwardingMetadata( - targetAgent, - parentOperationContext, - ); - - // Get the UI stream writer from operationContext if available - const uiStreamWriter = parentOperationContext?.systemContext?.get( - "uiStreamWriter", - ) as UIMessageStreamWriter>; - - // Get the fullStream writer from operationContext if available - const fullStreamWriter = parentOperationContext?.systemContext?.get( - "fullStreamWriter", - ) as WritableStreamDefaultWriter; - - // If we have a UI writer, merge the subagent's stream with metadata - if (uiStreamWriter && response.fullStream) { - // Convert the subagent's fullStream to UI message stream - // Include all messages to maintain tool call/result pairing - const subagentUIStream = response.toUIMessageStream(); - - // Wrap the stream with metadata enricher to add metadata to all parts - // Apply type filters from supervisor config - const enrichedStream = createMetadataEnrichedStream( - subagentUIStream, - forwardingMetadata, - this.supervisorConfig?.fullStreamEventForwarding?.types || ["tool-call", "tool-result"], + const { finalResult: streamResult, usage: streamUsage } = + await this.streamTextWithForwarding( + targetAgent, + messages, + baseOptions, + parentOperationContext, ); - - // Use the writer to merge the enriched stream - // This handles promise tracking and error handling automatically - uiStreamWriter.merge(enrichedStream); - } - - // If we have a fullStream writer, also write the subagent's fullStream events - if (fullStreamWriter && response.fullStream) { - // Get allowed event types from supervisor config - const allowedTypes = this.supervisorConfig?.fullStreamEventForwarding?.types || [ - "tool-call", - "tool-result", - ]; - - // Write subagent's fullStream events with metadata - const writeSubagentFullStream = async () => { - try { - for await (const part of response.fullStream) { - // Check if this event type should be forwarded - if (!shouldForwardChunk(part, allowedTypes)) { - continue; // Skip this event if not in allowed types - } - - // Add subagent metadata to each part - const enrichedPart = { - ...part, - ...forwardingMetadata, - }; - this.registerToolCallMetadata(parentOperationContext, enrichedPart, targetAgent); - await fullStreamWriter.write(enrichedPart); - } - } catch (error) { - // Log error but don't throw to avoid breaking the main flow - const logger = parentOperationContext?.logger || getGlobalLogger(); - logger.error("Error writing subagent fullStream", { error }); - } - }; - - // Start writing in background (don't await) - writeSubagentFullStream(); - } - - // Get the final result - check for bailed result first - const bailedResultFromContext = parentOperationContext?.systemContext?.get( - "bailedResult", - ) as { agentName: string; response: string } | undefined; - finalResult = bailedResultFromContext?.response || (await response.text); - usage = await response.usage; - - const assistantMessage: UIMessage = { - id: crypto.randomUUID(), - role: "assistant", - parts: [{ type: "text", text: finalResult }], - }; - finalMessages = [taskMessage, assistantMessage]; + finalResult = streamResult; + usage = streamUsage; + finalMessages = [taskMessage, this.createAssistantMessage(finalResult)]; } else if (this.isStreamTextConfig(targetAgentConfig)) { // StreamText configuration const options: StreamTextOptions = { ...baseOptions, ...targetAgentConfig.options }; - const response = await targetAgent.streamText(messages, options); - const forwardingMetadata = this.buildForwardingMetadata( - targetAgent, - parentOperationContext, - ); - - // Get the UI stream writer from operationContext if available - const uiStreamWriter = parentOperationContext?.systemContext?.get( - "uiStreamWriter", - ) as UIMessageStreamWriter>; - - // Get the fullStream writer from operationContext if available - const fullStreamWriter = parentOperationContext?.systemContext?.get( - "fullStreamWriter", - ) as WritableStreamDefaultWriter; - - // If we have a UI writer, merge the subagent's UI stream with metadata - if (uiStreamWriter && response.fullStream) { - // Convert the subagent's fullStream to UI message stream - // Include original messages to maintain tool call/result pairing - const subagentUIStream = response.toUIMessageStream(); - - // Wrap the stream with metadata enricher to add metadata to all parts - // Apply type filters from supervisor config - const enrichedStream = createMetadataEnrichedStream( - subagentUIStream, - forwardingMetadata, - this.supervisorConfig?.fullStreamEventForwarding?.types || ["tool-call", "tool-result"], + const { finalResult: streamResult, usage: streamUsage } = + await this.streamTextWithForwarding( + targetAgent, + messages, + options, + parentOperationContext, ); - - // Use the writer to merge the enriched stream - // This handles promise tracking and error handling automatically - uiStreamWriter.merge(enrichedStream); - } - - // If we have a fullStream writer, also write the subagent's fullStream events - if (fullStreamWriter && response.fullStream) { - // Get allowed event types from supervisor config - const allowedTypes = this.supervisorConfig?.fullStreamEventForwarding?.types || [ - "tool-call", - "tool-result", - ]; - - // Write subagent's fullStream events with metadata - const writeSubagentFullStream = async () => { - try { - for await (const part of response.fullStream) { - // Check if this event type should be forwarded - if (!shouldForwardChunk(part, allowedTypes)) { - continue; // Skip this event if not in allowed types - } - - // Add subagent metadata to each part - const enrichedPart = { - ...part, - ...forwardingMetadata, - }; - this.registerToolCallMetadata(parentOperationContext, enrichedPart, targetAgent); - await fullStreamWriter.write(enrichedPart); - } - } catch (error) { - // Log error but don't throw to avoid breaking the main flow - const logger = parentOperationContext?.logger || getGlobalLogger(); - logger.error("Error writing subagent fullStream", { error }); - } - }; - - // Start writing in background (don't await) - writeSubagentFullStream(); - } - - // Get the final result - check for bailed result first - const bailedResultFromContext = parentOperationContext?.systemContext?.get( - "bailedResult", - ) as { agentName: string; response: string } | undefined; - finalResult = bailedResultFromContext?.response || (await response.text); - usage = await response.usage; - - const assistantMessage: UIMessage = { - id: crypto.randomUUID(), - role: "assistant", - parts: [{ type: "text", text: finalResult }], - }; - finalMessages = [taskMessage, assistantMessage]; + finalResult = streamResult; + usage = streamUsage; + finalMessages = [taskMessage, this.createAssistantMessage(finalResult)]; } else if (this.isGenerateTextConfig(targetAgentConfig)) { // GenerateText configuration const options: GenerateTextOptions = { ...baseOptions, ...targetAgentConfig.options }; const response = await targetAgent.generateText(messages, options); finalResult = response.text; usage = response.usage; - - const assistantMessage: UIMessage = { - id: crypto.randomUUID(), - role: "assistant", - parts: [{ type: "text", text: finalResult }], - }; - finalMessages = [taskMessage, assistantMessage]; + finalMessages = [taskMessage, this.createAssistantMessage(finalResult)]; } else if (this.isStreamObjectConfig(targetAgentConfig)) { // StreamObject configuration const options: StreamObjectOptions = { ...baseOptions, ...targetAgentConfig.options }; @@ -585,13 +431,7 @@ ${task}\n\nContext: ${safeStringify(contextObj, { indentation: 2 })}`; ); const finalObject = await response.object; finalResult = safeStringify(finalObject); - - const assistantMessage: UIMessage = { - id: crypto.randomUUID(), - role: "assistant", - parts: [{ type: "text", text: finalResult }], - }; - finalMessages = [taskMessage, assistantMessage]; + finalMessages = [taskMessage, this.createAssistantMessage(finalResult)]; } else if (this.isGenerateObjectConfig(targetAgentConfig)) { // GenerateObject configuration const options: GenerateObjectOptions = { ...baseOptions, ...targetAgentConfig.options }; @@ -602,13 +442,7 @@ ${task}\n\nContext: ${safeStringify(contextObj, { indentation: 2 })}`; ); finalResult = safeStringify(response); usage = (response as any).usage; - - const assistantMessage: UIMessage = { - id: crypto.randomUUID(), - role: "assistant", - parts: [{ type: "text", text: finalResult }], - }; - finalMessages = [taskMessage, assistantMessage]; + finalMessages = [taskMessage, this.createAssistantMessage(finalResult)]; } else { // This should never happen due to exhaustive type checking throw new Error("Unknown subagent configuration type"); @@ -731,6 +565,131 @@ ${task}\n\nContext: ${safeStringify(contextObj, { indentation: 2 })}`; } } + private createAssistantMessage(text: string): UIMessage { + return { + id: crypto.randomUUID(), + role: "assistant", + parts: [{ type: "text", text }], + }; + } + + private getStreamWriters(parentOperationContext?: OperationContext): { + uiStreamWriter?: UIMessageStreamWriter>; + fullStreamWriter?: WritableStreamDefaultWriter; + } { + const uiStreamWriter = parentOperationContext?.systemContext?.get( + "uiStreamWriter", + ) as UIMessageStreamWriter>; + const fullStreamWriter = parentOperationContext?.systemContext?.get( + "fullStreamWriter", + ) as WritableStreamDefaultWriter; + return { uiStreamWriter, fullStreamWriter }; + } + + private forwardStreamEvents( + response: Awaited>, + forwardingMetadata: AgentMetadataContextValue, + parentOperationContext: OperationContext | undefined, + targetAgent: Agent, + ): void { + const { uiStreamWriter, fullStreamWriter } = this.getStreamWriters(parentOperationContext); + + // If we have a UI writer, merge the subagent's stream with metadata + if (uiStreamWriter && response.fullStream) { + // Convert the subagent's fullStream to UI message stream + // Include all messages to maintain tool call/result pairing + const subagentUIStream = response.toUIMessageStream(); + + // Wrap the stream with metadata enricher to add metadata to all parts + // Apply type filters from supervisor config + const enrichedStream = createMetadataEnrichedStream( + subagentUIStream, + forwardingMetadata, + this.supervisorConfig?.fullStreamEventForwarding?.types || ["tool-call", "tool-result"], + ); + + // Use the writer to merge the enriched stream + // This handles promise tracking and error handling automatically + uiStreamWriter.merge(enrichedStream); + } + + // If we have a fullStream writer, also write the subagent's fullStream events + if (fullStreamWriter && response.fullStream) { + this.writeFullStream( + response, + forwardingMetadata, + parentOperationContext, + targetAgent, + fullStreamWriter, + ); + } + } + + private writeFullStream( + response: Awaited>, + forwardingMetadata: AgentMetadataContextValue, + parentOperationContext: OperationContext | undefined, + targetAgent: Agent, + fullStreamWriter: WritableStreamDefaultWriter, + ): void { + // Get allowed event types from supervisor config + const allowedTypes = this.supervisorConfig?.fullStreamEventForwarding?.types || [ + "tool-call", + "tool-result", + ]; + + // Write subagent's fullStream events with metadata + const writeSubagentFullStream = async () => { + try { + for await (const part of response.fullStream) { + // Check if this event type should be forwarded + if (!shouldForwardChunk(part, allowedTypes)) { + continue; // Skip this event if not in allowed types + } + + // Add subagent metadata to each part + const enrichedPart = { + ...part, + ...forwardingMetadata, + }; + this.registerToolCallMetadata(parentOperationContext, enrichedPart, targetAgent); + await fullStreamWriter.write(enrichedPart); + } + } catch (error) { + // Log error but don't throw to avoid breaking the main flow + const logger = parentOperationContext?.logger || getGlobalLogger(); + logger.error("Error writing subagent fullStream", { error }); + } + }; + + // Start writing in background (don't await) + writeSubagentFullStream(); + } + + private async resolveStreamResult( + response: Awaited>, + parentOperationContext?: OperationContext, + ): Promise<{ finalResult: string; usage: any }> { + const bailedResultFromContext = parentOperationContext?.systemContext?.get("bailedResult") as + | { agentName: string; response: string } + | undefined; + const finalResult = bailedResultFromContext?.response || (await response.text); + const usage = await response.usage; + return { finalResult, usage }; + } + + private async streamTextWithForwarding( + targetAgent: Agent, + messages: UIMessage[], + options: StreamTextOptions, + parentOperationContext: OperationContext | undefined, + ): Promise<{ finalResult: string; usage: any }> { + const response = await targetAgent.streamText(messages, options); + const forwardingMetadata = this.buildForwardingMetadata(targetAgent, parentOperationContext); + this.forwardStreamEvents(response, forwardingMetadata, parentOperationContext, targetAgent); + return this.resolveStreamResult(response, parentOperationContext); + } + /** * Hand off a task to multiple agents in parallel */ diff --git a/packages/langfuse-exporter/src/exporter.ts b/packages/langfuse-exporter/src/exporter.ts index b07cd4cb0..74d40197c 100644 --- a/packages/langfuse-exporter/src/exporter.ts +++ b/packages/langfuse-exporter/src/exporter.ts @@ -89,6 +89,16 @@ type LangfuseExporterParams = { debug?: boolean; } & LangfuseOptions; +type TraceInfo = { + rootSpan?: ReadableSpan; + traceName?: string; + userId?: string; + sessionId?: string; + tags?: string[]; + langfuseTraceId?: string; + updateParent: boolean; +}; + export class LangfuseExporter implements SpanExporter { private readonly langfuse: Langfuse; private readonly debug: boolean; @@ -144,7 +154,7 @@ export class LangfuseExporter implements SpanExporter { } } - private processTraceSpans(traceId: string, spans: ReadableSpan[]): void { + private extractTraceInfo(spans: ReadableSpan[]): TraceInfo { let rootSpan: ReadableSpan | undefined = undefined; let traceName: string | undefined = undefined; let userId: string | undefined = undefined; @@ -188,10 +198,32 @@ export class LangfuseExporter implements SpanExporter { if (attrs.langfuseUpdateParent != null) updateParent = Boolean(attrs.langfuseUpdateParent); } - const finalTraceId = langfuseTraceId ?? traceId; - traceName = traceName ?? rootSpan?.name ?? `Trace ${finalTraceId.substring(0, 8)}`; + return { + rootSpan, + traceName, + userId, + sessionId, + tags, + langfuseTraceId, + updateParent, + }; + } - // Create Langfuse Trace - only include trace-level fields if updateParent is true + private buildTraceParams( + finalTraceId: string, + traceName: string, + traceInfo: TraceInfo, + ): { + id: string; + name?: string; + userId?: string; + sessionId?: string; + tags?: string[]; + metadata?: Record; + input?: any; + output?: any; + model?: string; + } { const traceParams: { id: string; name?: string; @@ -204,22 +236,46 @@ export class LangfuseExporter implements SpanExporter { model?: string; } = { id: finalTraceId }; - if (updateParent) { + if (traceInfo.updateParent) { traceParams.name = traceName; - traceParams.userId = userId; - traceParams.sessionId = sessionId; - traceParams.tags = tags; + traceParams.userId = traceInfo.userId; + traceParams.sessionId = traceInfo.sessionId; + traceParams.tags = traceInfo.tags; traceParams.input = safeJsonParse( - String(rootSpan?.attributes["ai.prompt.messages"] ?? rootSpan?.attributes?.input ?? null), + String( + traceInfo.rootSpan?.attributes["ai.prompt.messages"] ?? + traceInfo.rootSpan?.attributes?.input ?? + null, + ), ); traceParams.output = safeJsonParse( - String(rootSpan?.attributes["ai.response.text"] ?? rootSpan?.attributes?.output ?? null), + String( + traceInfo.rootSpan?.attributes["ai.response.text"] ?? + traceInfo.rootSpan?.attributes?.output ?? + null, + ), ); // Add combined metadata from root span? Let's extract from root if available. - traceParams.metadata = rootSpan ? extractMetadata(rootSpan.attributes) : undefined; - traceParams.model = String(rootSpan?.attributes["ai.model.name"]) ?? undefined; + traceParams.metadata = traceInfo.rootSpan + ? extractMetadata(traceInfo.rootSpan.attributes) + : undefined; + const modelName = traceInfo.rootSpan?.attributes["ai.model.name"]; + traceParams.model = modelName != null ? String(modelName) : undefined; } + return traceParams; + } + + private processTraceSpans(traceId: string, spans: ReadableSpan[]): void { + const traceInfo = this.extractTraceInfo(spans); + + const finalTraceId = traceInfo.langfuseTraceId ?? traceId; + const traceName = + traceInfo.traceName ?? traceInfo.rootSpan?.name ?? `Trace ${finalTraceId.substring(0, 8)}`; + + // Create Langfuse Trace - only include trace-level fields if updateParent is true + const traceParams = this.buildTraceParams(finalTraceId, traceName, traceInfo); + this.logDebug(`Creating/Updating Langfuse trace ${finalTraceId}`, traceParams); this.langfuse.trace(traceParams); diff --git a/packages/server-core/src/edge.ts b/packages/server-core/src/edge.ts index 67d923244..aa22b20d4 100644 --- a/packages/server-core/src/edge.ts +++ b/packages/server-core/src/edge.ts @@ -9,15 +9,7 @@ export { LOG_ROUTES, } from "./routes/definitions"; -export { - handleGetAgents, - handleGenerateText, - handleStreamText, - handleChatStream, - handleResumeChatStream, - handleGenerateObject, - handleStreamObject, -} from "./handlers/agent.handlers"; +export * from "./handlers/agent.handlers"; export { handleGetAgent, diff --git a/packages/server-core/src/websocket/setup.ts b/packages/server-core/src/websocket/setup.ts index 94f4d3f90..cfca7e329 100644 --- a/packages/server-core/src/websocket/setup.ts +++ b/packages/server-core/src/websocket/setup.ts @@ -56,6 +56,142 @@ function hasWebSocketConsoleAccess(req: IncomingMessage, url: URL): boolean { return false; } +type WebSocketAuthResult = { + user: any | null; + handled: boolean; +}; + +function closeUpgrade(socket: Socket, statusLine: string): void { + socket.end(`${statusLine}\r\n\r\n`); +} + +function denyUnauthorized(socket: Socket): WebSocketAuthResult { + closeUpgrade(socket, "HTTP/1.1 401 Unauthorized"); + return { user: null, handled: true }; +} + +function denyServerError(socket: Socket): WebSocketAuthResult { + closeUpgrade(socket, "HTTP/1.1 500 Internal Server Error"); + return { user: null, handled: true }; +} + +async function verifyTokenIfPresent( + provider: AuthProvider, + token: string | null, +): Promise { + if (!token) { + return null; + } + try { + return await provider.verifyToken(token); + } catch { + return null; + } +} + +async function verifyTokenOrReject(params: { + provider: AuthProvider; + token: string | null; + socket: Socket; + logger?: Logger; + missingTokenLog: string; +}): Promise { + const { provider, token, socket, logger, missingTokenLog } = params; + + if (!token) { + logger?.debug(missingTokenLog); + return denyUnauthorized(socket); + } + + try { + const user = await provider.verifyToken(token); + return { user, handled: false }; + } catch (error) { + logger?.debug("[WebSocket] Token verification failed:", { error }); + return denyUnauthorized(socket); + } +} + +async function resolveAuthNextUser(params: { + auth: AuthNextConfig | AuthProvider; + path: string; + req: IncomingMessage; + url: URL; + socket: Socket; + logger?: Logger; +}): Promise { + const { auth, path, req, url, socket, logger } = params; + const config = normalizeAuthNextConfig(auth); + const provider = config.provider; + const access = resolveAuthNextAccess("WS", path, config); + + if (access === "public") { + const user = await verifyTokenIfPresent(provider, url.searchParams.get("token")); + return { user, handled: false }; + } + + if (access === "console") { + const hasAccess = hasWebSocketConsoleAccess(req, url); + if (!hasAccess) { + logger?.debug("[WebSocket] Unauthorized console connection attempt"); + return denyUnauthorized(socket); + } + return { user: { id: "console", type: "console-access" }, handled: false }; + } + + if (isWebSocketDevBypass(req, url)) { + // Dev bypass for local testing + return { user: null, handled: false }; + } + + return verifyTokenOrReject({ + provider, + token: url.searchParams.get("token"), + socket, + logger, + missingTokenLog: "[WebSocket] No token provided for protected WebSocket", + }); +} + +async function resolveLegacyAuthUser(params: { + auth: AuthProvider; + path: string; + req: IncomingMessage; + url: URL; + socket: Socket; + logger?: Logger; +}): Promise { + const { auth, path, req, url, socket, logger } = params; + + if (path.includes("/observability")) { + const hasAccess = hasWebSocketConsoleAccess(req, url); + if (!hasAccess) { + logger?.debug("[WebSocket] Unauthorized observability connection attempt"); + return denyUnauthorized(socket); + } + return { user: { id: "console", type: "console-access" }, handled: false }; + } + + const hasConsoleAccess = hasWebSocketConsoleAccess(req, url); + if (hasConsoleAccess) { + return { user: { id: "console", type: "console-access" }, handled: false }; + } + + const needsAuth = requiresAuth("WS", path, auth.publicRoutes, auth.defaultPrivate); + if (needsAuth) { + return verifyTokenOrReject({ + provider: auth, + token: url.searchParams.get("token"), + socket, + logger, + missingTokenLog: "[WebSocket] No token provided for protected WebSocket", + }); + } + + const user = await verifyTokenIfPresent(auth, url.searchParams.get("token")); + return { user, handled: false }; +} + /** * Create and configure a WebSocket server * @param deps Server provider dependencies @@ -97,119 +233,34 @@ export function setupWebSocketUpgrade( const url = new URL(req.url || "", "http://localhost"); const path = url.pathname; - if (path.startsWith(pathPrefix)) { - let user: any = null; - - // Check authentication if auth provider is configured - if (auth) { - try { - if (isAuthNextConfig(auth)) { - const config = normalizeAuthNextConfig(auth); - const provider = config.provider; - const access = resolveAuthNextAccess("WS", path, config); - - if (access === "public") { - const token = url.searchParams.get("token"); - if (token) { - try { - user = await provider.verifyToken(token); - } catch { - // Ignore token errors on public routes - } - } - } else if (access === "console") { - const hasAccess = hasWebSocketConsoleAccess(req, url); - if (!hasAccess) { - logger?.debug("[WebSocket] Unauthorized console connection attempt"); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - user = { id: "console", type: "console-access" }; - } else { - if (isWebSocketDevBypass(req, url)) { - // Dev bypass for local testing - } else { - const token = url.searchParams.get("token"); - if (token) { - try { - user = await provider.verifyToken(token); - } catch (error) { - logger?.debug("[WebSocket] Token verification failed:", { error }); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - } else { - logger?.debug("[WebSocket] No token provided for protected WebSocket"); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - } - } - } else { - // Legacy auth flow - if (path.includes("/observability")) { - const hasAccess = hasWebSocketConsoleAccess(req, url); - if (!hasAccess) { - logger?.debug("[WebSocket] Unauthorized observability connection attempt"); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - user = { id: "console", type: "console-access" }; - } else { - const hasConsoleAccess = hasWebSocketConsoleAccess(req, url); - if (hasConsoleAccess) { - user = { id: "console", type: "console-access" }; - } else { - const needsAuth = requiresAuth("WS", path, auth.publicRoutes, auth.defaultPrivate); - - if (needsAuth) { - const token = url.searchParams.get("token"); - if (token) { - try { - user = await auth.verifyToken(token); - } catch (error) { - logger?.debug("[WebSocket] Token verification failed:", { error }); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - } else { - logger?.debug("[WebSocket] No token provided for protected WebSocket"); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - } else { - const token = url.searchParams.get("token"); - if (token) { - try { - user = await auth.verifyToken(token); - } catch { - // Ignore token errors on public routes - } - } - } - } - } - } - } catch (error) { - logger?.error("[WebSocket] Auth error:", { error }); - socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); - socket.destroy(); + if (!path.startsWith(pathPrefix)) { + socket.destroy(); + return; + } + + let user: any = null; + + // Check authentication if auth provider is configured + if (auth) { + try { + const authResult = isAuthNextConfig(auth) + ? await resolveAuthNextUser({ auth, path, req, url, socket, logger }) + : await resolveLegacyAuthUser({ auth, path, req, url, socket, logger }); + + if (authResult.handled) { return; } + user = authResult.user; + } catch (error) { + logger?.error("[WebSocket] Auth error:", { error }); + denyServerError(socket); + return; } - - // Proceed with WebSocket upgrade - wss.handleUpgrade(req, socket, head, (websocket) => { - wss.emit("connection", websocket, req, user); - }); - } else { - socket.destroy(); } + + // Proceed with WebSocket upgrade + wss.handleUpgrade(req, socket, head, (websocket) => { + wss.emit("connection", websocket, req, user); + }); }); } diff --git a/packages/vercel-ai-exporter/package.json b/packages/vercel-ai-exporter/package.json index d9b1a790a..a5e5fad31 100644 --- a/packages/vercel-ai-exporter/package.json +++ b/packages/vercel-ai-exporter/package.json @@ -54,7 +54,7 @@ "build": "tsup", "dev": "tsup --watch", "lint": "biome check src/", - "lint:fix": "biome check --apply src/", + "lint:fix": "biome check --write src/", "publint": "publint --strict", "test": "vitest", "test:coverage": "vitest run --coverage" diff --git a/website/package.json b/website/package.json index 7ce00b284..30890fd5b 100644 --- a/website/package.json +++ b/website/package.json @@ -10,7 +10,7 @@ "docusaurus": "docusaurus", "lint": "biome check .", "lint:ci": "biome ci .", - "lint:fix": "biome check . --apply", + "lint:fix": "biome check . --write", "lint:staged": "lint-staged", "serve": "docusaurus serve", "start": "docusaurus start", @@ -21,7 +21,7 @@ }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ - "biome check --apply --no-errors-on-unmatched" + "biome check --write --no-errors-on-unmatched" ], "*.{md,mdx}": [ "prettier --config ../.prettierrc --write" @@ -77,7 +77,7 @@ "usehooks-ts": "^3.1.0" }, "devDependencies": { - "@biomejs/biome": "1.6.1", + "@biomejs/biome": "^1.9.4", "@docusaurus/module-type-aliases": "3.1.1", "@docusaurus/tsconfig": "3.1.1", "@docusaurus/types": "3.1.1", diff --git a/website/src/components/supervisor-agent/flow-version.tsx b/website/src/components/supervisor-agent/flow-version.tsx index 8fb542cc2..3d83aa242 100644 --- a/website/src/components/supervisor-agent/flow-version.tsx +++ b/website/src/components/supervisor-agent/flow-version.tsx @@ -338,13 +338,6 @@ export const FlowVersion = ({ isVisible }: FlowVersionProps) => { const [__, setAnimationStep] = useState(0); const [isAnimating, setIsAnimating] = useState(false); - // In VSCode, this may be shown as `suppressions/unused`, - // but running the biome command directly works fine. - // This happens because, when opening the `voltagent` directory in VSCode, - // the LSP uses the biome config from the project root directory. - // This is likely because the `website` directory is not a pnpm monorepo, - // so it doesn't have its own biome config. - // biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when setEdges is called const startAnimation = useCallback(() => { setIsAnimating(true); setAnimationStep(0); diff --git a/website/src/components/tutorial/mobile-agent-code.tsx b/website/src/components/tutorial/mobile-agent-code.tsx index 341824abf..2d26c6c27 100644 --- a/website/src/components/tutorial/mobile-agent-code.tsx +++ b/website/src/components/tutorial/mobile-agent-code.tsx @@ -140,7 +140,6 @@ export const MobileAgentCode = ({ isVisible }: MobileAgentCodeProps) => { {/* Agent Creation */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} { {/* Agent Name */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} handleFeatureClick("name", e)} @@ -169,7 +167,6 @@ export const MobileAgentCode = ({ isVisible }: MobileAgentCodeProps) => { {/* Agent Description & Instructions */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} { {/* Model Selection */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} handleFeatureClick("model", e)} @@ -205,7 +201,6 @@ export const MobileAgentCode = ({ isVisible }: MobileAgentCodeProps) => { {/* Server Setup */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} handleFeatureClick("server", e)} diff --git a/website/src/components/tutorial/mobile-tool-code.tsx b/website/src/components/tutorial/mobile-tool-code.tsx index 5217c24ae..74412fee6 100644 --- a/website/src/components/tutorial/mobile-tool-code.tsx +++ b/website/src/components/tutorial/mobile-tool-code.tsx @@ -158,7 +158,6 @@ export const MobileToolCode = ({ isVisible }: MobileToolCodeProps) => { {/* Tool Definition */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} { {/* Tool Name & Description */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} { {/* Parameters */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} { {/* Execute Function */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} { {/* Agent Creation */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */} handleFeatureClick("agent", e)} diff --git a/website/src/components/workflows/animation-diagram.tsx b/website/src/components/workflows/animation-diagram.tsx index b12a2323f..553913834 100644 --- a/website/src/components/workflows/animation-diagram.tsx +++ b/website/src/components/workflows/animation-diagram.tsx @@ -1,18 +1,667 @@ -import { - ArrowPathIcon, - BoltIcon, - CpuChipIcon, - CubeIcon, - UserCircleIcon, -} from "@heroicons/react/24/outline"; +import { ArrowPathIcon, CpuChipIcon, UserCircleIcon } from "@heroicons/react/24/outline"; import { useMediaQuery } from "@site/src/hooks/use-media-query"; -import React, { useState, useEffect, useRef, useCallback } from "react"; +import type React from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { AnimatedBeam } from "../magicui/animated-beam"; interface WorkflowCodeExampleProps { isVisible: boolean; } +// Unified color scheme - softer, less eye-straining colors +const colors = { + user: { + border: "border-emerald-400/70", + text: "text-emerald-300", + shadow: "shadow-[0_0_15px_rgba(52,211,153,0.3)]", + activeShadow: "shadow-[0_0_20px_rgba(52,211,153,0.5)]", + beam: "#34d399", + beamOpacity: "rgba(52, 211, 153, 0.4)", + }, + workflow: { + border: "border-rose-400/70", + text: "text-rose-300", + shadow: "shadow-[0_0_15px_rgba(251,113,133,0.3)]", + activeShadow: "shadow-[0_0_20px_rgba(251,113,133,0.5)]", + beam: "#fb7185", + beamOpacity: "rgba(251, 113, 133, 0.4)", + }, + agentA: { + border: "border-blue-400/70", + text: "text-blue-300", + shadow: "shadow-[0_0_15px_rgba(96,165,250,0.3)]", + activeShadow: "shadow-[0_0_20px_rgba(96,165,250,0.5)]", + beam: "#60a5fa", + beamOpacity: "rgba(96, 165, 250, 0.4)", + }, + agentB: { + border: "border-teal-400/70", + text: "text-teal-300", + shadow: "shadow-[0_0_15px_rgba(45,212,191,0.3)]", + activeShadow: "shadow-[0_0_20px_rgba(45,212,191,0.5)]", + beam: "#2dd4bf", + beamOpacity: "rgba(45, 212, 191, 0.4)", + }, + codeHighlight: { + step: "text-purple-400", + entity: "text-blue-400", + softGlow: "text-shadow-neon", + }, +}; + +type NodeRef = React.RefObject; + +interface WorkflowCodeBlockProps { + animationStep: number; +} + +const WorkflowCodeBlock = ({ animationStep }: WorkflowCodeBlockProps) => { + const isAgentStep = animationStep === 3 || animationStep === 4; + + return ( +
+
+        
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
+ + + createWorkflowChain + + () +
+ + .andAll + + ({"{"} +
+ id: + "fetch-user-data-steps" + , +
+ steps: [ +
+ + andAgent + + ( + + contentAgent + + ), +
+ + andAgent + + ( + + analysisAgent + + ), +
+ ], +
+ {"}"} + ) +
+
+
+
+ ); +}; + +interface WorkflowDiagramProps { + isMobile: boolean; + animationStep: number; + showFullDiagram: boolean; + diagramRef: NodeRef; + userNodeRef: NodeRef; + workflowNodeRef: NodeRef; + agentOneNodeRef: NodeRef; + agentTwoNodeRef: NodeRef; +} + +const WorkflowDiagram = ({ + isMobile, + animationStep, + showFullDiagram, + diagramRef, + userNodeRef, + workflowNodeRef, + agentOneNodeRef, + agentTwoNodeRef, +}: WorkflowDiagramProps) => { + return ( +
+
+ + +
+
+ ); +}; + +interface WorkflowNodesProps { + isMobile: boolean; + animationStep: number; + userNodeRef: NodeRef; + workflowNodeRef: NodeRef; + agentOneNodeRef: NodeRef; + agentTwoNodeRef: NodeRef; +} + +const WorkflowNodes = ({ + isMobile, + animationStep, + userNodeRef, + workflowNodeRef, + agentOneNodeRef, + agentTwoNodeRef, +}: WorkflowNodesProps) => { + const isUserActive = animationStep === 5 || animationStep === 6; + const isWorkflowActive = animationStep >= 2; + const isWorkflowSpinning = animationStep > 2; + const isAgentActive = animationStep === 3; + + const agentOnePosition = isMobile + ? "top-[70%] left-[30%] transform -translate-x-1/2" + : "top-[25%] left-[75%] transform -translate-y-1/2"; + const agentTwoPosition = isMobile + ? "top-[70%] left-[70%] transform -translate-x-1/2" + : "top-[75%] left-[75%] transform -translate-y-1/2"; + + return ( + <> + + + + + + ); +}; + +interface UserNodeProps { + nodeRef: NodeRef; + isMobile: boolean; + isActive: boolean; +} + +const UserNode = ({ nodeRef, isMobile, isActive }: UserNodeProps) => { + const positionClass = isMobile + ? "top-[10%] left-1/2 transform -translate-x-1/2" + : "top-1/2 left-[10%] transform -translate-y-1/2"; + + return ( +
+ + user +
+ ); +}; + +interface WorkflowNodeProps { + nodeRef: NodeRef; + isMobile: boolean; + isActive: boolean; + isSpinning: boolean; +} + +const WorkflowNode = ({ nodeRef, isMobile, isActive, isSpinning }: WorkflowNodeProps) => { + const positionClass = isMobile + ? "top-[40%] left-1/2 transform -translate-x-1/2" + : "top-1/2 left-[40%] transform -translate-y-1/2"; + + return ( +
+ + workflow +
+ ); +}; + +type NodeColors = { + border: string; + text: string; + shadow: string; + activeShadow: string; +}; + +interface AgentNodeProps { + nodeRef: NodeRef; + positionClass: string; + colors: NodeColors; + label: string; + isActive: boolean; + containerClassName?: string; + labelClassName?: string; +} + +const AgentNode = ({ + nodeRef, + positionClass, + colors: nodeColors, + label, + isActive, + containerClassName, + labelClassName, +}: AgentNodeProps) => { + return ( +
+ + {label} +
+ ); +}; + +interface WorkflowBeamsProps { + isMobile: boolean; + animationStep: number; + diagramRef: NodeRef; + userNodeRef: NodeRef; + workflowNodeRef: NodeRef; + agentOneNodeRef: NodeRef; + agentTwoNodeRef: NodeRef; +} + +const WorkflowBeams = ({ + isMobile, + animationStep, + diagramRef, + userNodeRef, + workflowNodeRef, + agentOneNodeRef, + agentTwoNodeRef, +}: WorkflowBeamsProps) => { + return ( + <> + + + + + + + + ); +}; + +interface BeamProps { + containerRef: NodeRef; + fromRef: NodeRef; + toRef: NodeRef; + isMobile: boolean; + animationStep: number; +} + +const UserToWorkflowBeam = ({ + containerRef, + fromRef, + toRef, + isMobile, + animationStep, +}: BeamProps) => { + if (animationStep < 2) { + return null; + } + + const isRequest = animationStep === 2; + const beamColor = isRequest ? colors.workflow.beam : "transparent"; + + return ( + 2} + /> + ); +}; + +const WorkflowToAgentOneBeam = ({ + containerRef, + fromRef, + toRef, + isMobile, + animationStep, +}: BeamProps) => { + if (animationStep < 3) { + return null; + } + + const showBeam = animationStep <= 4; + const beamColor = showBeam ? colors.agentA.beam : "transparent"; + + return ( + 3} + /> + ); +}; + +const AgentOneToWorkflowBeam = ({ + containerRef, + fromRef, + toRef, + isMobile, + animationStep, +}: BeamProps) => { + if (animationStep < 4) { + return null; + } + + const showBeam = animationStep <= 4; + const beamColor = showBeam ? colors.agentA.beam : "transparent"; + + return ( + 4} + /> + ); +}; + +const WorkflowToAgentTwoBeam = ({ + containerRef, + fromRef, + toRef, + isMobile, + animationStep, +}: BeamProps) => { + if (animationStep < 3) { + return null; + } + + const showBeam = animationStep <= 4; + const beamColor = showBeam ? colors.agentB.beam : "transparent"; + + return ( + 3} + /> + ); +}; + +const AgentTwoToWorkflowBeam = ({ + containerRef, + fromRef, + toRef, + isMobile, + animationStep, +}: BeamProps) => { + if (animationStep < 4) { + return null; + } + + const showBeam = animationStep <= 4; + const beamColor = showBeam ? colors.agentB.beam : "transparent"; + + return ( + 4 && animationStep < 6} + /> + ); +}; + +const WorkflowToUserBeam = ({ + containerRef, + fromRef, + toRef, + isMobile, + animationStep, +}: BeamProps) => { + if (animationStep < 5) { + return null; + } + + return ( + + ); +}; + export function WorkflowCodeExample({ isVisible }: WorkflowCodeExampleProps) { const [animationStep, setAnimationStep] = useState(0); const [isAnimating, setIsAnimating] = useState(false); @@ -21,47 +670,6 @@ export function WorkflowCodeExample({ isVisible }: WorkflowCodeExampleProps) { const diagramRef = useRef(null); const isMobile = useMediaQuery("(max-width: 768px)"); - // Unified color scheme - softer, less eye-straining colors - const colors = { - user: { - border: "border-emerald-400/70", - text: "text-emerald-300", - shadow: "shadow-[0_0_15px_rgba(52,211,153,0.3)]", - activeShadow: "shadow-[0_0_20px_rgba(52,211,153,0.5)]", - beam: "#34d399", - beamOpacity: "rgba(52, 211, 153, 0.4)", - }, - workflow: { - border: "border-rose-400/70", - text: "text-rose-300", - shadow: "shadow-[0_0_15px_rgba(251,113,133,0.3)]", - activeShadow: "shadow-[0_0_20px_rgba(251,113,133,0.5)]", - beam: "#fb7185", - beamOpacity: "rgba(251, 113, 133, 0.4)", - }, - agentA: { - border: "border-blue-400/70", - text: "text-blue-300", - shadow: "shadow-[0_0_15px_rgba(96,165,250,0.3)]", - activeShadow: "shadow-[0_0_20px_rgba(96,165,250,0.5)]", - beam: "#60a5fa", - beamOpacity: "rgba(96, 165, 250, 0.4)", - }, - agentB: { - border: "border-teal-400/70", - text: "text-teal-300", - shadow: "shadow-[0_0_15px_rgba(45,212,191,0.3)]", - activeShadow: "shadow-[0_0_20px_rgba(45,212,191,0.5)]", - beam: "#2dd4bf", - beamOpacity: "rgba(45, 212, 191, 0.4)", - }, - codeHighlight: { - step: "text-purple-400", - entity: "text-blue-400", - softGlow: "text-shadow-neon", - }, - }; - // Refs for beam connections const userNodeRef = useRef(null); const workflowNodeRef = useRef(null); @@ -160,348 +768,19 @@ export function WorkflowCodeExample({ isVisible }: WorkflowCodeExampleProps) {
{/* Code Section - Left Side */} -
-
-                
-
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
- - - createWorkflowChain - - () -
- - .andAll - - ({"{"} -
- id: - "fetch-user-data-steps" - , -
- steps: [ -
- - andAgent - - ( - - contentAgent - - ), -
- - andAgent - - ( - - analysisAgent - - ), -
- ], -
- {"}"} - ) -
-
-
-
+ {/* Diagram Section - Right Side */} -
-
- {/* User Node */} -
- - user -
- - {/* Workflow Node */} -
= 2 ? colors.workflow.activeShadow : colors.workflow.shadow - }`} - > - 2 ? "animate-spin" : "" - }`} - /> - workflow -
- - {/* Agent One Node */} -
- - contentAgent -
- - {/* Agent Two Node */} -
- - analysisAgent -
- - {/* Animated Beams */} - {/* User to Workflow - Initial Request */} - {animationStep >= 2 && ( - 2 ? "transparent" : colors.workflow.beam} - gradientStopColor={animationStep > 2 ? "transparent" : colors.workflow.beam} - particleColor={animationStep > 2 ? "transparent" : colors.workflow.beam} - delay={0.1} - duration={3} - curvature={0} - startXOffset={isMobile ? 0 : 10} - endXOffset={isMobile ? 0 : -10} - particleSize={3} - particleCount={3} - particleSpeed={3.0} - showParticles={animationStep === 2} - solidBeamAfterRequest={animationStep > 2} - /> - )} - - {/* Workflow to Agent One - Request */} - {animationStep >= 3 && ( - 4 ? "transparent" : colors.agentA.beam} - gradientStopColor={animationStep > 4 ? "transparent" : colors.agentA.beam} - particleColor={animationStep > 4 ? "transparent" : colors.agentA.beam} - delay={0.1} - duration={6} - curvature={isMobile ? -20 : -20} - startXOffset={isMobile ? 0 : 10} - endXOffset={isMobile ? 0 : -10} - particleCount={1} - showParticles={animationStep <= 4} - pathType={"angular"} - particleSpeed={3.0} - particleSize={3} - particleDuration={animationStep === 3 ? 3.2 : 0} - solidBeamAfterRequest={animationStep > 3} - /> - )} - - {/* Agent One to Workflow - Response */} - {animationStep >= 4 && ( - 4 ? "transparent" : colors.agentA.beam} - gradientStopColor={animationStep > 4 ? "transparent" : colors.agentA.beam} - particleColor={animationStep > 4 ? "transparent" : colors.agentA.beam} - delay={0.2} - duration={6} - curvature={isMobile ? -20 : -20} - reverse={true} - startXOffset={isMobile ? 0 : -10} - endXOffset={isMobile ? 0 : 10} - particleCount={1} - showParticles={animationStep <= 4} - pathType={"angular"} - particleSpeed={3.0} - particleSize={3} - particleDuration={animationStep === 4 ? 3.2 : 0} - solidBeamAfterRequest={animationStep > 4} - /> - )} - - {/* Workflow to Agent Two - Request */} - {animationStep >= 3 && ( - 4 ? "transparent" : colors.agentB.beam} - gradientStopColor={animationStep > 4 ? "transparent" : colors.agentB.beam} - particleColor={animationStep > 4 ? "transparent" : colors.agentB.beam} - delay={0.1} - duration={6} - curvature={isMobile ? 20 : 20} - startXOffset={isMobile ? 0 : 10} - endXOffset={isMobile ? 0 : -10} - pathType={"angular"} - particleCount={1} - showParticles={animationStep <= 4} - particleSpeed={3.0} - particleSize={3} - particleDuration={animationStep === 3 ? 3.2 : 0} - solidBeamAfterRequest={animationStep > 3} - /> - )} - - {/* Agent Two to Workflow - Response */} - {animationStep >= 4 && ( - 4 ? "transparent" : colors.agentB.beam} - gradientStopColor={animationStep > 4 ? "transparent" : colors.agentB.beam} - particleColor={animationStep > 4 ? "transparent" : colors.agentB.beam} - delay={0.1} - duration={6} - curvature={isMobile ? 20 : 20} - reverse={true} - startXOffset={isMobile ? 0 : -10} - endXOffset={isMobile ? 0 : 10} - pathType={"angular"} - particleCount={1} - showParticles={animationStep <= 4} - particleSpeed={3.0} - particleSize={3} - particleDuration={animationStep === 4 ? 3.2 : 0} - solidBeamAfterRequest={animationStep > 4 && animationStep < 6} - /> - )} - - {/* Workflow to User - Final Response */} - {animationStep >= 5 && ( - - )} -
-
+
diff --git a/website/src/pages/tutorial/chatbot-problem.tsx b/website/src/pages/tutorial/chatbot-problem.tsx index f1fa0e54f..ac7a5ce11 100644 --- a/website/src/pages/tutorial/chatbot-problem.tsx +++ b/website/src/pages/tutorial/chatbot-problem.tsx @@ -485,7 +485,6 @@ export default function ChatbotProblemTutorial() {
{/* Card 1 - Tool Definition */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: ignore */}
{ + const remainingText = rawText.trim(); + if (!remainingText) { + return; + } + + if (remainingText.includes(":") && remainingText.includes(";")) { + const bulletSections: string[][] = []; + const textParts: string[] = []; + + const sentences = remainingText.split(". "); + + for (const sentence of sentences) { + if (sentence.includes(":") && sentence.includes(";")) { + const colonIndex = sentence.indexOf(":"); + const beforeColon = sentence.substring(0, colonIndex + 1); + const afterColon = sentence.substring(colonIndex + 1); + + if (beforeColon.trim()) { + textParts.push(beforeColon.trim()); + } + + if (afterColon.trim()) { + const bulletPoints = afterColon + .split(";") + .map((point) => point.trim()) + .filter((point) => point); + bulletSections.push(bulletPoints); + } + } else if (sentence.trim()) { + textParts.push(sentence.trim() + (sentence === sentences[sentences.length - 1] ? "" : ".")); + } + } + + for (let j = 0; j < Math.max(textParts.length, bulletSections.length); j++) { + if (j < textParts.length && textParts[j]) { + parts.push({ + type: "text", + content: textParts[j], + }); + } + if (j < bulletSections.length && bulletSections[j] && bulletSections[j].length > 0) { + parts.push({ + type: "bullets", + content: bulletSections[j], + }); + } + } + return; + } + + parts.push({ type: "text", content: remainingText }); +}; + +const parseSolutionContent = (content: string): CaseStudyContentPart[] => { + const parts: CaseStudyContentPart[] = []; + let currentText = ""; + let inQuote = false; + let quoteText = ""; + + for (let i = 0; i < content.length; i++) { + const char = content[i]; + + if (char === '"' && !inQuote) { + appendTextSections(currentText, parts); + currentText = ""; + inQuote = true; + quoteText = ""; + continue; + } + + if (char === '"' && inQuote) { + if (quoteText.trim()) { + parts.push({ + type: "quote", + content: quoteText.trim(), + }); + quoteText = ""; + } + inQuote = false; + continue; + } + + if (inQuote) { + quoteText += char; + } else { + currentText += char; + } + } + + appendTextSections(currentText, parts); + return parts; +}; + export default function CustomerProjectPage({ customer }: CustomerProjectPageProps): JSX.Element { if (!customer) { return ( @@ -331,196 +430,47 @@ export default function CustomerProjectPage({ customer }: CustomerProjectPagePro The Solution: VoltAgent + VoltOps
- {(() => { - const content = customer.case_study.solution_paragraph; - const parts = []; - let currentText = ""; - let inQuote = false; - let quoteText = ""; - - for (let i = 0; i < content.length; i++) { - const char = content[i]; - - if (char === '"' && !inQuote) { - // Starting a quote - if (currentText.trim()) { - // Handle multiple bullet point sections - const remainingText = currentText.trim(); - - // Look for patterns like "text: bullet; bullet; bullet. Text: bullet; bullet; bullet" - const bulletSections = []; - const textParts = []; - - // Split by periods to find sections - const sentences = remainingText.split(". "); - - for (const sentence of sentences) { - if (sentence.includes(":") && sentence.includes(";")) { - // This sentence has bullet points - const colonIndex = sentence.indexOf(":"); - const beforeColon = sentence.substring(0, colonIndex + 1); - const afterColon = sentence.substring(colonIndex + 1); - - if (beforeColon.trim()) { - textParts.push(beforeColon.trim()); - } - - if (afterColon.trim()) { - const bulletPoints = afterColon - .split(";") - .map((point) => point.trim()) - .filter((point) => point); - bulletSections.push(bulletPoints); - } - } else if (sentence.trim()) { - // Regular text - textParts.push( - sentence.trim() + - (sentence === sentences[sentences.length - 1] ? "" : "."), - ); - } - } - - // Add text and bullet sections alternately - for ( - let j = 0; - j < Math.max(textParts.length, bulletSections.length); - j++ - ) { - if (j < textParts.length && textParts[j]) { - parts.push({ - type: "text", - content: textParts[j], - }); - } - if (j < bulletSections.length && bulletSections[j]) { - parts.push({ - type: "bullets", - content: bulletSections[j], - }); - } - } - - currentText = ""; - } - inQuote = true; - quoteText = ""; - } else if (char === '"' && inQuote) { - // Ending a quote - if (quoteText.trim()) { - parts.push({ - type: "quote", - content: quoteText.trim(), - }); - quoteText = ""; - } - inQuote = false; - } else if (inQuote) { - quoteText += char; - } else { - currentText += char; - } - } - - if (currentText.trim()) { - // Handle remaining text with same logic - const remainingText = currentText.trim(); - - if (remainingText.includes(":") && remainingText.includes(";")) { - const bulletSections = []; - const textParts = []; - - const sentences = remainingText.split(". "); - - for (const sentence of sentences) { - if (sentence.includes(":") && sentence.includes(";")) { - const colonIndex = sentence.indexOf(":"); - const beforeColon = sentence.substring(0, colonIndex + 1); - const afterColon = sentence.substring(colonIndex + 1); - - if (beforeColon.trim()) { - textParts.push(beforeColon.trim()); - } - - if (afterColon.trim()) { - const bulletPoints = afterColon - .split(";") - .map((point) => point.trim()) - .filter((point) => point); - bulletSections.push(bulletPoints); - } - } else if (sentence.trim()) { - textParts.push( - sentence.trim() + - (sentence === sentences[sentences.length - 1] ? "" : "."), - ); - } - } - - for ( - let j = 0; - j < Math.max(textParts.length, bulletSections.length); - j++ - ) { - if (j < textParts.length && textParts[j]) { - parts.push({ type: "text", content: textParts[j] }); - } - if (j < bulletSections.length && bulletSections[j]) { - parts.push({ - type: "bullets", - content: bulletSections[j], - }); - } - } - } else { - parts.push({ type: "text", content: remainingText }); - } - } - - return parts.map((part) => { - if (part.type === "quote") { - return ( -
- "{part.content}" -
- ); - } - if (part.type === "bullets") { - return ( -
    - {part.content.map((bullet, bulletIndex) => ( -
  • - {bullet} -
  • - ))} -
- ); - } + {parseSolutionContent(customer.case_study.solution_paragraph).map((part) => { + if (part.type === "quote") { return ( -

- {part.content} -

+ "{part.content}" + ); - }); - })()} + } + if (part.type === "bullets") { + return ( +
    + {part.content.map((bullet, bulletIndex) => ( +
  • + {bullet} +
  • + ))} +
+ ); + } + return ( +

+ {part.content} +

+ ); + })}