diff --git a/convex.json b/convex.json index 8cd4cfde..350a4985 100644 --- a/convex.json +++ b/convex.json @@ -1,7 +1,7 @@ { - "$schema": "./node_modules/convex/schemas/convex.schema.json", "functions": "example/convex", "codegen": { "legacyComponentApi": false - } + }, + "$schema": "./node_modules/convex/schemas/convex.schema.json" } diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 835cc4f1..24150fcf 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -47,6 +47,13 @@ import type * as usage_tracking_tables from "../usage_tracking/tables.js"; import type * as usage_tracking_usageHandler from "../usage_tracking/usageHandler.js"; import type * as utils from "../utils.js"; import type * as workflows_chaining from "../workflows/chaining.js"; +import type * as workflows_human_in_the_loop from "../workflows/human_in_the_loop.js"; +import type * as workflows_orchestrator from "../workflows/orchestrator.js"; +import type * as workflows_parallel from "../workflows/parallel.js"; +import type * as workflows_rateLimiting from "../workflows/rateLimiting.js"; +import type * as workflows_reason_act_cycle from "../workflows/reason_act_cycle.js"; +import type * as workflows_routing from "../workflows/routing.js"; +import type * as workflows_vector_routing from "../workflows/vector_routing.js"; import type { ApiFromModules, @@ -94,6 +101,13 @@ declare const fullApi: ApiFromModules<{ "usage_tracking/usageHandler": typeof usage_tracking_usageHandler; utils: typeof utils; "workflows/chaining": typeof workflows_chaining; + "workflows/human_in_the_loop": typeof workflows_human_in_the_loop; + "workflows/orchestrator": typeof workflows_orchestrator; + "workflows/parallel": typeof workflows_parallel; + "workflows/rateLimiting": typeof workflows_rateLimiting; + "workflows/reason_act_cycle": typeof workflows_reason_act_cycle; + "workflows/routing": typeof workflows_routing; + "workflows/vector_routing": typeof workflows_vector_routing; }>; /** diff --git a/example/convex/workflows/human_in_the_loop.ts b/example/convex/workflows/human_in_the_loop.ts new file mode 100644 index 00000000..1a3b4561 --- /dev/null +++ b/example/convex/workflows/human_in_the_loop.ts @@ -0,0 +1,257 @@ +// See the docs at https://docs.convex.dev/agents/human-agents +import { + WorkflowManager, + defineEvent, + vWorkflowId, +} from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { + internalAction, + internalMutation, + mutation, +} from "../_generated/server"; +import { v } from "convex/values"; +import { createThread, saveMessage, stepCountIs } from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { agent as simpleAgent } from "../agents/simple"; +import { tool } from "ai"; +import { z } from "zod/v3"; + +/** + * Human-in-the-Loop Pattern: Pause generation for human input + * + * This demonstrates doing generation until a human's input is required, which + * is accomplished via: + * 1. A tool call with no execute handler + * 2. Creating an event for the parent workflow to wait on with ctx.awaitEvent + * 3. A human providing the response which sends the event + */ + +const workflow = new WorkflowManager(components.workflow); + +// Define an event for human approval +export const humanInputEvent = defineEvent({ + name: "humanInput" as const, + validator: v.object({ + response: v.string(), + toolCallId: v.string(), + }), +}); + +// Tool without execute handler - this will pause execution +export const askForApproval = tool({ + description: + "Request approval from a human before proceeding with a sensitive action", + inputSchema: z.object({ + action: z + .string() + .describe("The action that needs approval (e.g., 'delete data')"), + reason: z.string().describe("Why this action is necessary"), + }), +}); + +export const humanInTheLoopWorkflow = workflow.define({ + args: { task: v.string(), threadId: v.string() }, + returns: v.string(), + handler: async (ctx, args): Promise => { + console.log("Starting human-in-the-loop workflow for task:", args.task); + + // Step 1: Do initial generation with the tool available + const initialMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: args.task, + }); + + const initialResult = await ctx.runAction( + internal.workflows.human_in_the_loop.generateWithApprovalTool, + { + promptMessageId: initialMsg.messageId, + threadId: args.threadId, + workflowId: ctx.workflowId, + }, + { retry: true }, + ); + + console.log("Initial generation result:", initialResult); + + // Step 2: Check if human approval was requested + if (initialResult.approvalRequests.length > 0) { + console.log( + "Human approval required:", + initialResult.approvalRequests.length, + "requests", + ); + + // Wait for each approval request + for (const request of initialResult.approvalRequests) { + console.log("Waiting for approval:", request); + + // Wait for the human to respond via the event + const humanInput = await ctx.awaitEvent(humanInputEvent); + + console.log("Human response received:", humanInput); + + // Save the human's response as a tool result + await simpleAgent.saveMessage(ctx, { + threadId: args.threadId, + message: { + role: "tool", + content: [ + { + type: "tool-result", + output: { type: "text", value: humanInput.response }, + toolCallId: humanInput.toolCallId, + toolName: "askForApproval", + }, + ], + }, + metadata: { + provider: "human", + providerMetadata: { + human: { role: "approver" }, + }, + }, + }); + } + + // Step 3: Continue generation with the human's responses + const finalResult = await ctx.runAction( + internal.workflows.human_in_the_loop.continueGeneration, + { + promptMessageId: initialResult.promptMessageId!, + threadId: args.threadId, + }, + { retry: true }, + ); + + return finalResult.text; + } else { + // No approval needed, return the initial response + return initialResult.text; + } + }, +}); + +// Generate text with approval tool available +export const generateWithApprovalTool = internalAction({ + args: { + promptMessageId: v.string(), + threadId: v.string(), + workflowId: vWorkflowId, + }, + handler: async (ctx, args) => { + const result = await simpleAgent.generateText( + ctx, + { threadId: args.threadId }, + { + promptMessageId: args.promptMessageId, + tools: { askForApproval }, + prompt: `You are a helpful assistant. If the task involves sensitive actions like deleting data, modifying important settings, or making irreversible changes, you MUST use the askForApproval tool to get human approval before proceeding. Be specific about what action you're requesting approval for.`, + stopWhen: stepCountIs(3), + }, + ); + + // Extract approval requests from tool calls + const approvalRequests = result.toolCalls + .filter((tc) => tc.toolName === "askForApproval" && !tc.dynamic) + .map(({ toolCallId, input }) => ({ + toolCallId, + action: input.action, + reason: input.reason, + })); + + // If there are approval requests, create events for each + if (approvalRequests.length > 0) { + await ctx.runMutation( + internal.workflows.human_in_the_loop.notifyHumanApproval, + { + workflowId: args.workflowId, + threadId: args.threadId, + approvalRequests, + }, + ); + } + + return { + text: result.text, + promptMessageId: result.promptMessageId, + approvalRequests, + }; + }, +}); + +// Notify that human approval is needed (could send email, notification, etc.) +export const notifyHumanApproval = internalMutation({ + args: { + workflowId: vWorkflowId, + threadId: v.string(), + approvalRequests: v.array( + v.object({ + toolCallId: v.string(), + action: v.string(), + reason: v.string(), + }), + ), + }, + handler: async (_ctx, args) => { + // In a real app, this would: + // - Send notifications to appropriate humans + // - Store pending approvals in a table + // - Create UI for humans to respond + console.log("Human approval needed for workflow:", args.workflowId); + console.log("Approval requests:", args.approvalRequests); + console.log( + "Call humanResponse mutation with workflowId and responses to proceed", + ); + }, +}); + +// Continue generation after human input +export const continueGeneration = simpleAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Public mutation for humans to provide their response +export const humanResponse = mutation({ + args: { + workflowId: vWorkflowId, + toolCallId: v.string(), + approved: v.boolean(), + comments: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const response = args.approved + ? `Approved: ${args.comments ?? "You may proceed with this action."}` + : `Rejected: ${args.comments ?? "This action is not authorized."}`; + + await workflow.sendEvent(ctx, { + ...humanInputEvent, + workflowId: args.workflowId, + value: { + response, + toolCallId: args.toolCallId, + }, + }); + }, +}); + +// Mutation to start the human-in-the-loop workflow +export const startHumanInTheLoop = mutation({ + args: { task: v.string() }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: `Human-in-Loop: ${args.task.slice(0, 50)}`, + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.human_in_the_loop.humanInTheLoopWorkflow, + { task: args.task, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/convex/workflows/orchestrator.ts b/example/convex/workflows/orchestrator.ts new file mode 100644 index 00000000..7721a421 --- /dev/null +++ b/example/convex/workflows/orchestrator.ts @@ -0,0 +1,153 @@ +// See the docs at https://docs.convex.dev/agents/workflows +import { WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { mutation } from "../_generated/server"; +import { v } from "convex/values"; +import { + Agent, + createThread, + saveMessage, + stepCountIs, +} from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { weatherAgent } from "../agents/weather"; +import { fashionAgent } from "../agents/fashion"; +import { storyAgent } from "../agents/story"; +import { agent as simpleAgent } from "../agents/simple"; +import { z } from "zod/v4"; +import { defaultConfig } from "convex/agents/config"; + +/** + * Orchestrator Pattern: One agent decides what to do and composes other agents + * + * This workflow demonstrates using an LLM to route a user request to the + * appropriate specialist agent, then executing that agent's workflow. + */ + +const workflow = new WorkflowManager(components.workflow); + +export const orchestratorWorkflow = workflow.define({ + args: { prompt: v.string(), threadId: v.string() }, + handler: async (ctx, args): Promise => { + // Step 1: Use an LLM to determine which agent should handle the request + const { object: routing } = await ctx.runAction( + internal.workflows.orchestrator.routeRequest, + { prompt: args.prompt }, + { retry: true }, + ); + + console.log("Routing decision:", routing); + + // Step 2: Execute the appropriate agent based on the routing decision + const questionMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: args.prompt, + }); + + let result: string; + switch (routing.agent) { + case "weather": { + const weatherResult = await ctx.runAction( + internal.workflows.orchestrator.getWeatherInfo, + { promptMessageId: questionMsg.messageId, threadId: args.threadId }, + { retry: true }, + ); + result = weatherResult.text; + break; + } + case "fashion": { + const fashionResult = await ctx.runAction( + internal.workflows.orchestrator.getFashionInfo, + { promptMessageId: questionMsg.messageId, threadId: args.threadId }, + { retry: true }, + ); + result = fashionResult.text; + break; + } + case "story": { + const storyResult = await ctx.runAction( + internal.workflows.orchestrator.getStory, + { promptMessageId: questionMsg.messageId, threadId: args.threadId }, + { retry: true }, + ); + result = storyResult.text; + break; + } + default: { + const generalResult = await ctx.runAction( + internal.workflows.orchestrator.getGeneralResponse, + { promptMessageId: questionMsg.messageId, threadId: args.threadId }, + { retry: true }, + ); + result = generalResult.text; + break; + } + } + + console.log("Orchestrator result:", result); + return result; + }, +}); + +// Routing agent action - decides which specialist to use +const routingAgent = new Agent(components.agent, { + name: "Routing Agent", + ...defaultConfig, + instructions: `You are a routing agent. Analyze the user's request and determine which specialist agent should handle it: +- "weather": For questions about weather, forecasts, or climate +- "fashion": For questions about clothing, style, or what to wear +- "story": For requests to tell a story or create a narrative +- "general": For all other requests + +Return the agent name and a brief reason for your choice.`, +}); +export const routeRequest = routingAgent.asObjectAction({ + schema: z.object({ + agent: z.union([ + z.literal("weather"), + z.literal("fashion"), + z.literal("story"), + z.literal("general"), + ]), + reason: z.string(), + }), +}); + +// Specialist agent actions +export const getWeatherInfo = weatherAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +export const getFashionInfo = fashionAgent.asTextAction({ + stopWhen: stepCountIs(5), +}); + +export const getStory = storyAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +export const getGeneralResponse = simpleAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +// Mutation to start the orchestrator workflow +// TODO: make this a loop until it decides it's done +export const startOrchestrator = mutation({ + args: { prompt: v.string() }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: "Orchestrator: " + args.prompt.slice(0, 50), + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.orchestrator.orchestratorWorkflow, + { prompt: args.prompt, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/convex/workflows/parallel.ts b/example/convex/workflows/parallel.ts new file mode 100644 index 00000000..d40603a0 --- /dev/null +++ b/example/convex/workflows/parallel.ts @@ -0,0 +1,148 @@ +// See the docs at https://docs.convex.dev/agents/workflows +import { WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { mutation } from "../_generated/server"; +import { v } from "convex/values"; +import { createThread, saveMessage, stepCountIs } from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { weatherAgent } from "../agents/weather"; +import { fashionAgent } from "../agents/fashion"; + +/** + * Parallel Pattern: Execute multiple workflows concurrently and combine results + * + * This workflow demonstrates kicking off parallel nested workflows to do + * multiple things at once, then combining their results. + */ + +const workflow = new WorkflowManager(components.workflow); + +export const parallelWorkflow = workflow.define({ + args: { location: v.string(), threadId: v.string() }, + returns: v.object({ + weather: v.string(), + fashion: v.string(), + summary: v.string(), + }), + handler: async ( + ctx, + args, + ): Promise<{ weather: string; fashion: string; summary: string }> => { + console.log("Starting parallel workflow for", args.location); + + // Kick off multiple nested workflows in parallel + const [weather, fashion] = await Promise.all([ + ctx.runWorkflow(internal.workflows.parallel.weatherSubWorkflow, { + location: args.location, + threadId: args.threadId, + }), + ctx.runWorkflow(internal.workflows.parallel.fashionSubWorkflow, { + location: args.location, + threadId: args.threadId, + }), + ]); + + console.log("Weather result:", weather); + console.log("Fashion result:", fashion); + + // Now combine the results with a summary + const summary = await ctx.runAction( + internal.workflows.parallel.summarizeResults, + { + threadId: args.threadId, + prompt: `Summarize the weather and fashion advice from the conversation history into a concise travel recommendation: + Weather: ${weather} + Fashion: ${fashion} + Travel recommendation: + `, + }, + { retry: true }, + ); + + return { + weather, + fashion, + summary: summary.text, + }; + }, +}); + +// Sub-workflow for weather information +export const weatherSubWorkflow = workflow.define({ + args: { location: v.string(), threadId: v.string() }, + returns: v.string(), + handler: async (ctx, args): Promise => { + const questionMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: `What is the weather forecast for ${args.location}?`, + }); + + const result = await ctx.runAction( + internal.workflows.parallel.getWeather, + { + promptMessageId: questionMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + return result.text; + }, +}); + +// Sub-workflow for fashion advice +export const fashionSubWorkflow = workflow.define({ + args: { location: v.string(), threadId: v.string() }, + returns: v.string(), + handler: async (ctx, args): Promise => { + const questionMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: `Based on the weather discussion, what should someone wear in ${args.location}?`, + }); + + const result = await ctx.runAction( + internal.workflows.parallel.getFashion, + { + promptMessageId: questionMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + return result.text; + }, +}); + +// Agent actions +export const getWeather = weatherAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +export const getFashion = fashionAgent.asTextAction({ + stopWhen: stepCountIs(5), +}); + +export const summarizeResults = weatherAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Mutation to start the parallel workflow +export const startParallel = mutation({ + args: { location: v.string() }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: `Parallel: ${args.location}`, + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.parallel.parallelWorkflow, + { location: args.location, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/convex/workflows/rateLimiting.ts b/example/convex/workflows/rateLimiting.ts new file mode 100644 index 00000000..4d0bdb78 --- /dev/null +++ b/example/convex/workflows/rateLimiting.ts @@ -0,0 +1,148 @@ +// See the docs at https://docs.convex.dev/agents/workflows +import { WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { mutation } from "../_generated/server"; +import { v } from "convex/values"; +import { createThread, saveMessage, stepCountIs } from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { weatherAgent } from "../agents/weather"; +import { RateLimiter, SECOND } from "@convex-dev/rate-limiter"; + +/** + * Rate Limiting Pattern: Using rate limiter within workflows + * + * This workflow demonstrates using the rate limiter component from a workflow + * and using the returned runAfter to schedule the next step with proper delays. + */ + +const workflow = new WorkflowManager(components.workflow); + +// Create a rate limiter for API calls +export const apiRateLimiter = new RateLimiter(components.rateLimiter, { + weatherApiCalls: { + kind: "token bucket", + period: 10 * SECOND, + rate: 3, // 3 calls per 10 seconds + capacity: 3, + }, +}); + +export const rateLimitedWorkflow = workflow.define({ + args: { locations: v.array(v.string()), threadId: v.string() }, + returns: v.array(v.string()), + handler: async (ctx, args) => { + console.log( + "Starting rate-limited workflow for locations:", + args.locations, + ); + + const results: string[] = []; + + // Process each location with rate limiting + for (const location of args.locations) { + console.log("Processing location:", location); + + // Check the rate limit before making the API call + const rateLimit = await apiRateLimiter.check(ctx, "weatherApiCalls", { + key: "workflow-user", + }); + + console.log("Rate limit check:", rateLimit); + + // If we need to wait, use runAfter to schedule the next step + let runAfter: number | undefined = undefined; + if (!rateLimit.ok && rateLimit.retryAfter) { + console.log( + `Rate limit hit, scheduling after ${rateLimit.retryAfter}ms`, + ); + runAfter = rateLimit.retryAfter; + } + + // Save the question message + const questionMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: `What is the weather in ${location}?`, + }); + + // Make the API call with rate limiting + await apiRateLimiter.limit(ctx, "weatherApiCalls", { + key: "workflow-user", + reserve: true, + }); + const { text: result } = await ctx.runAction( + internal.workflows.rateLimiting.getWeatherWithRateLimit, + { + promptMessageId: questionMsg.messageId, + threadId: args.threadId, + userId: "workflow-user", + }, + { + retry: true, + runAfter, // Schedule after rate limit window + }, + ); + + results.push(result); + console.log(`Completed ${location}:`, result); + } + + // Summarize all the weather reports + const summaryMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: "Summarize all the weather information from our conversation.", + }); + + const { text: summary } = await ctx.runAction( + internal.workflows.rateLimiting.summarize, + { + promptMessageId: summaryMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + results.push(summary); + return results; + }, +}); + +// Check rate limit mutation +export const checkRateLimit = mutation({ + args: { userId: v.string() }, + handler: async (ctx, args) => { + return await apiRateLimiter.check(ctx, "weatherApiCalls", { + key: args.userId, + }); + }, +}); + +// Weather agent action with rate limiting +export const getWeatherWithRateLimit = weatherAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +// Summarization action +export const summarize = weatherAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Mutation to start the rate-limited workflow +export const startRateLimited = mutation({ + args: { locations: v.array(v.string()) }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: `Rate Limited: ${args.locations.join(", ")}`, + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.rateLimiting.rateLimitedWorkflow, + { locations: args.locations, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/convex/workflows/reason_act_cycle.ts b/example/convex/workflows/reason_act_cycle.ts new file mode 100644 index 00000000..c0035776 --- /dev/null +++ b/example/convex/workflows/reason_act_cycle.ts @@ -0,0 +1,183 @@ +// See the docs at https://docs.convex.dev/agents/workflows +import { WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { mutation } from "../_generated/server"; +import { v } from "convex/values"; +import { + Agent, + createThread, + saveMessage, + stepCountIs, +} from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { defaultConfig } from "convex/agents/config"; +import { z } from "zod/v4"; + +/** + * Reason-Act Cycle Pattern: Iterative reasoning and action loop + * + * This workflow demonstrates the pattern of: + * 1. Reasoning to decide what to do next + * 2. Taking the action(s) + * 3. Reasoning again based on results + * 4. Repeat until goal is achieved + */ + +const workflow = new WorkflowManager(components.workflow); + +export const reasonActCycleWorkflow = workflow.define({ + args: { goal: v.string(), threadId: v.string() }, + returns: v.object({ + cycles: v.number(), + finalAnswer: v.string(), + }), + handler: async ( + ctx, + args, + ): Promise<{ cycles: number; finalAnswer: string }> => { + console.log("Starting reason-act cycle for goal:", args.goal); + + let cycleCount = 0; + const maxCycles = 3; + let shouldContinue = true; + + // Initial reasoning step + let reasoningMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: `Goal: ${args.goal}\n\nAnalyze this goal and determine what information or actions you need to accomplish it. Decide on the next action.`, + }); + + while (shouldContinue && cycleCount < maxCycles) { + cycleCount++; + console.log(`Cycle ${cycleCount}: Reasoning`); + + // Step 1: Reason about what to do next + const { object: reasoning } = await ctx.runAction( + internal.workflows.reason_act_cycle.reasonAboutNextAction, + { + promptMessageId: reasoningMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + console.log("Reasoning result:", reasoning); + + // Step 2: Check if we should continue or if we have the answer + if (reasoning.action === "answer") { + shouldContinue = false; + console.log("Goal achieved, providing final answer"); + break; + } + + // Step 3: Take the action based on reasoning + const actionMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: `Execute this action: ${reasoning.action}\nRationale: ${reasoning.rationale}`, + }); + + const actionResult = await ctx.runAction( + internal.workflows.reason_act_cycle.executeAction, + { + promptMessageId: actionMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + console.log(`Action result:`, actionResult); + + // Step 4: Reason about the results and decide next steps + reasoningMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: `Based on the action we just took, analyze the results and determine if we've accomplished the goal or what to do next.`, + }); + } + + // Generate final answer + const finalMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: + "Based on all our reasoning and actions, provide a comprehensive final answer to the original goal.", + }); + + const finalAnswer = await ctx.runAction( + internal.workflows.reason_act_cycle.generateFinalAnswer, + { + promptMessageId: finalMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + return { + cycles: cycleCount, + finalAnswer: finalAnswer.text, + }; + }, +}); + +// Agent action for reasoning about next steps +const reasoningAgent = new Agent(components.agent, { + name: "Reasoning Agent", + ...defaultConfig, + instructions: `You are a reasoning agent. Analyze the conversation and goal, then decide what action to take next: +- "get_weather": If you need weather information +- "get_location": If you need to determine a location +- "analyze_data": If you need to process or analyze information you have +- "answer": If you have enough information to answer the goal + +Provide your rationale for the chosen action.`, +}); + +export const reasonAboutNextAction = reasoningAgent.asObjectAction({ + schema: z.object({ + action: z.enum(["get_weather", "get_location", "analyze_data", "answer"]), + rationale: z.string(), + }), +}); + +// Agent action for executing actions +const actionAgent = new Agent(components.agent, { + name: "Action Agent", + ...defaultConfig, + instructions: + "Execute the requested action using your available tools and knowledge. Provide detailed results.", +}); + +export const executeAction = actionAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +// Agent action for generating the final answer +const finalAnswerAgent = new Agent(components.agent, { + name: "Final Answer Agent", + ...defaultConfig, + instructions: + "Synthesize all the reasoning and actions taken to provide a comprehensive final answer to the original goal.", +}); + +export const generateFinalAnswer = finalAnswerAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Mutation to start the reason-act cycle workflow +export const startReasonActCycle = mutation({ + args: { goal: v.string() }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: `Reason-Act: ${args.goal.slice(0, 50)}`, + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.reason_act_cycle.reasonActCycleWorkflow, + { goal: args.goal, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/convex/workflows/routing.ts b/example/convex/workflows/routing.ts new file mode 100644 index 00000000..dc81724c --- /dev/null +++ b/example/convex/workflows/routing.ts @@ -0,0 +1,254 @@ +// See the docs at https://docs.convex.dev/agents/workflows +import { WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { mutation } from "../_generated/server"; +import { v } from "convex/values"; +import { + Agent, + createThread, + saveMessage, + stepCountIs, +} from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { agent as simpleAgent } from "../agents/simple"; +import { defaultConfig } from "convex/agents/config"; +import { z } from "zod/v4"; + +/** + * Routing Pattern: Intent-based routing to different code paths + * + * This workflow demonstrates having an agent discern user intent and route + * to different code paths accordingly (e.g., filing a bug, requesting support, + * or talking to sales). + */ + +const workflow = new WorkflowManager(components.workflow); + +export const routingWorkflow = workflow.define({ + args: { userMessage: v.string(), threadId: v.string() }, + returns: v.object({ + intent: v.string(), + response: v.string(), + metadata: v.any(), + }), + handler: async ( + ctx, + args, + ): Promise<{ intent: string; response: string; metadata: any }> => { + console.log("Starting routing workflow for message:", args.userMessage); + + // Step 1: Classify the user's intent + const intentMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: args.userMessage, + }); + + const { object: classification } = await ctx.runAction( + internal.workflows.routing.classifyIntent, + { + promptMessageId: intentMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + + console.log("Intent classification:", classification); + + // Step 2: Route to appropriate handler based on intent + let response: string; + let metadata: any; + + switch (classification.intent) { + case "bug_report": { + const bugTicket = await ctx.runMutation( + internal.workflows.routing.createBugTicket, + { + description: args.userMessage, + severity: classification.confidence > 0.8 ? "high" : "medium", + }, + ); + response = await ctx.runAction( + internal.workflows.routing.handleBugReport, + { + threadId: args.threadId, + ticketId: bugTicket.ticketId, + }, + { retry: true }, + ); + metadata = bugTicket; + break; + } + + case "support_request": { + const supportTicket = await ctx.runMutation( + internal.workflows.routing.createSupportTicket, + { + description: args.userMessage, + priority: classification.confidence > 0.8 ? "high" : "normal", + }, + ); + response = await ctx.runAction( + internal.workflows.routing.handleSupportRequest, + { + threadId: args.threadId, + ticketId: supportTicket.ticketId, + }, + { retry: true }, + ); + metadata = supportTicket; + break; + } + case "sales_inquiry": { + const salesLead = await ctx.runMutation( + internal.workflows.routing.createSalesLead, + { + inquiry: args.userMessage, + source: "chat", + }, + ); + response = await ctx.runAction( + internal.workflows.routing.handleSalesInquiry, + { + threadId: args.threadId, + leadId: salesLead.leadId, + }, + { retry: true }, + ); + metadata = salesLead; + break; + } + case "general_question": + default: { + response = await ctx.runAction( + internal.workflows.routing.handleGeneralQuestion, + { + threadId: args.threadId, + }, + { retry: true }, + ); + metadata = { type: "general" }; + break; + } + } + + return { + intent: classification.intent, + response, + metadata, + }; + }, +}); + +// Intent classification action +const intentAgent = new Agent(components.agent, { + name: "Intent Classifier", + ...defaultConfig, + instructions: `You are an intent classification agent. Analyze the user's message and classify it into one of these categories: +- "bug_report": User is reporting a bug, error, or something not working correctly +- "support_request": User needs help, has a technical question, or needs assistance with the product +- "sales_inquiry": User is interested in pricing, plans, features for purchase, or wants to talk to sales +- "general_question": General questions, casual conversation, or unclear intent +`, +}); +export const classifyIntent = intentAgent.asObjectAction({ + schema: z.object({ + intent: z.union([ + z.literal("bug_report"), + z.literal("support_request"), + z.literal("sales_inquiry"), + z.literal("general_question"), + ]), + confidence: z.number(), + reasoning: z.string(), + }), +}); + +// Bug report handler +const bugAgent = new Agent(components.agent, { + name: "Bug Reporter", + ...defaultConfig, + instructions: `You are a bug triage assistant. The user has reported a bug and a ticket has been created. +Acknowledge the bug report, provide the ticket ID from the conversation, and ask for any additional details that might be helpful (steps to reproduce, error messages, screenshots, etc.).`, +}); +export const handleBugReport = bugAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Support request handler +const supportAgent = new Agent(components.agent, { + name: "Support Assistant", + ...defaultConfig, + instructions: `You are a technical support assistant. A support ticket has been created for the user. +Provide helpful information to address their question, and let them know a support ticket has been created with the ID from the conversation. Offer to help troubleshoot or provide documentation links.`, +}); +export const handleSupportRequest = supportAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +// Sales inquiry handler +const salesAgent = new Agent(components.agent, { + name: "Sales Agent", + ...defaultConfig, + instructions: `You are a sales assistant. The user is interested in learning more about the product or pricing. +Provide relevant information about features, pricing, or plans. Let them know a sales representative will follow up, and their inquiry has been logged with the ID from the conversation.`, +}); +export const handleSalesInquiry = salesAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// General question handler +export const handleGeneralQuestion = simpleAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Ticket/lead creation mutations +export const createBugTicket = mutation({ + args: { description: v.string(), severity: v.string() }, + handler: async (_ctx, args) => { + // In a real app, this would create a ticket in your bug tracking system + const ticketId = `BUG-${Date.now()}`; + console.log("Created bug ticket:", ticketId, args); + return { ticketId, severity: args.severity }; + }, +}); + +export const createSupportTicket = mutation({ + args: { description: v.string(), priority: v.string() }, + handler: async (_ctx, args) => { + // In a real app, this would create a ticket in your support system + const ticketId = `SUP-${Date.now()}`; + console.log("Created support ticket:", ticketId, args); + return { ticketId, priority: args.priority }; + }, +}); + +export const createSalesLead = mutation({ + args: { inquiry: v.string(), source: v.string() }, + handler: async (_ctx, args) => { + // In a real app, this would create a lead in your CRM + const leadId = `LEAD-${Date.now()}`; + console.log("Created sales lead:", leadId, args); + return { leadId, source: args.source }; + }, +}); + +// Mutation to start the routing workflow +export const startRouting = mutation({ + args: { userMessage: v.string() }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: `Routing: ${args.userMessage.slice(0, 50)}`, + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.routing.routingWorkflow, + { userMessage: args.userMessage, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/convex/workflows/vector_routing.ts b/example/convex/workflows/vector_routing.ts new file mode 100644 index 00000000..fd670999 --- /dev/null +++ b/example/convex/workflows/vector_routing.ts @@ -0,0 +1,293 @@ +// See the docs at https://docs.convex.dev/agents/workflows +import { WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "../_generated/api"; +import { internalQuery, mutation } from "../_generated/server"; +import { v } from "convex/values"; +import { createThread, saveMessage, stepCountIs } from "@convex-dev/agent"; +import { getAuthUserId } from "../utils"; +import { agent as simpleAgent } from "../agents/simple"; +import { weatherAgent } from "../agents/weather"; +import { storyAgent } from "../agents/story"; +import { embed } from "ai"; +import { textEmbeddingModel } from "../modelsForDemo"; + +/** + * Vector Routing Pattern: Use vector embeddings to route requests + * + * This workflow demonstrates using vector embeddings and semantic similarity + * to decide what to do next, rather than using an LLM for classification. + * This is faster and more deterministic than LLM-based routing. + */ + +const workflow = new WorkflowManager(components.workflow); + +// Predefined route templates with example prompts +const routeTemplates = [ + { + route: "weather", + description: "Weather forecasts and climate information", + examples: [ + "What's the weather like?", + "Will it rain tomorrow?", + "What's the forecast for this weekend?", + "How hot will it be today?", + "Should I bring an umbrella?", + ], + }, + { + route: "story", + description: "Creative storytelling and narratives", + examples: [ + "Tell me a story", + "Write a short tale about adventure", + "Create a narrative with a twist ending", + "I want to hear a creative story", + "Make up a story for me", + ], + }, + { + route: "support", + description: "Technical support and help", + examples: [ + "I need help with my account", + "Something isn't working", + "How do I use this feature?", + "I'm having trouble", + "Can you help me troubleshoot?", + ], + }, + { + route: "general", + description: "General questions and conversation", + examples: [ + "What can you do?", + "Tell me about yourself", + "How are you?", + "What's the meaning of life?", + "Explain quantum computing", + ], + }, +]; + +export const vectorRoutingWorkflow = workflow.define({ + args: { userMessage: v.string(), threadId: v.string() }, + returns: v.object({ + route: v.string(), + similarity: v.number(), + response: v.string(), + }), + handler: async ( + ctx, + args, + ): Promise<{ route: string; similarity: number; response: string }> => { + console.log("Starting vector routing workflow for:", args.userMessage); + + // Step 1: Get embedding for the user's message + const userEmbedding = await ctx.runQuery( + internal.workflows.vector_routing.getEmbedding, + { text: args.userMessage }, + ); + + console.log("User embedding generated"); + + // Step 2: Get embeddings for all route examples and find best match + const routeMatch = await ctx.runQuery( + internal.workflows.vector_routing.findBestRoute, + { + userEmbedding: userEmbedding.embedding, + routes: routeTemplates, + }, + ); + + console.log("Best route match:", routeMatch); + + // Step 3: Save the user's message + const userMsg = await saveMessage(ctx, components.agent, { + threadId: args.threadId, + prompt: args.userMessage, + }); + + // Step 4: Route to the appropriate handler based on vector similarity + let response: string; + switch (routeMatch.route) { + case "weather": { + const weatherResponse = await ctx.runAction( + internal.workflows.vector_routing.handleWeather, + { + promptMessageId: userMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + response = weatherResponse.text; + break; + } + + case "story": { + const storyResponse = await ctx.runAction( + internal.workflows.vector_routing.handleStory, + { + promptMessageId: userMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + response = storyResponse.text; + break; + } + + case "support": { + const supportResponse = await ctx.runAction( + internal.workflows.vector_routing.handleSupport, + { + promptMessageId: userMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + response = supportResponse.text; + break; + } + default: { + const generalResponse = await ctx.runAction( + internal.workflows.vector_routing.handleGeneral, + { + promptMessageId: userMsg.messageId, + threadId: args.threadId, + }, + { retry: true }, + ); + response = generalResponse.text; + break; + } + } + + return { + route: routeMatch.route, + similarity: routeMatch.similarity, + response, + }; + }, +}); + +// Get embedding for text +export const getEmbedding = internalQuery({ + args: { text: v.string() }, + handler: async (_ctx, args) => { + const result = await embed({ + model: textEmbeddingModel, + value: args.text, + }); + return { + embedding: result.embedding, + }; + }, +}); + +// Calculate cosine similarity between two vectors +function cosineSimilarity(a: number[], b: number[]): number { + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +// Find best matching route using vector similarity +export const findBestRoute = internalQuery({ + args: { + userEmbedding: v.array(v.number()), + routes: v.array( + v.object({ + route: v.string(), + description: v.string(), + examples: v.array(v.string()), + }), + ), + }, + handler: async (_ctx, args) => { + let bestRoute = "general"; + let bestSimilarity = 0; + + // For each route, compute average similarity across all examples + for (const routeTemplate of args.routes) { + let totalSimilarity = 0; + + for (const example of routeTemplate.examples) { + // Get embedding for this example + const exampleEmbedding = await embed({ + model: textEmbeddingModel, + value: example, + }); + + // Calculate similarity + const similarity = cosineSimilarity( + args.userEmbedding, + exampleEmbedding.embedding, + ); + totalSimilarity += similarity; + } + + // Average similarity for this route + const avgSimilarity = totalSimilarity / routeTemplate.examples.length; + + console.log( + `Route "${routeTemplate.route}" similarity: ${avgSimilarity}`, + ); + + if (avgSimilarity > bestSimilarity) { + bestSimilarity = avgSimilarity; + bestRoute = routeTemplate.route; + } + } + + return { + route: bestRoute, + similarity: bestSimilarity, + }; + }, +}); + +// Route handlers +export const handleWeather = weatherAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +export const handleStory = storyAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +export const handleSupport = simpleAgent.asTextAction({ + stopWhen: stepCountIs(3), +}); + +export const handleGeneral = simpleAgent.asTextAction({ + stopWhen: stepCountIs(2), +}); + +// Mutation to start the vector routing workflow +export const startVectorRouting = mutation({ + args: { userMessage: v.string() }, + handler: async ( + ctx, + args, + ): Promise<{ threadId: string; workflowId: string }> => { + const userId = await getAuthUserId(ctx); + const threadId = await createThread(ctx, components.agent, { + userId, + title: `Vector Routing: ${args.userMessage.slice(0, 50)}`, + }); + const workflowId = await workflow.start( + ctx, + internal.workflows.vector_routing.vectorRoutingWorkflow, + { userMessage: args.userMessage, threadId }, + ); + return { threadId, workflowId }; + }, +}); diff --git a/example/tsconfig.json b/example/tsconfig.json index a71fcacd..9fff2944 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -6,7 +6,7 @@ }, "baseUrl": ".", "lib": ["DOM", "DOM.Iterable", "ESNext"], - "skipLibCheck": false, + "skipLibCheck": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, diff --git a/package-lock.json b/package-lock.json index dc86a8cd..b02e1aba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "chokidar-cli": "3.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "convex": "1.29.0", + "convex": "1.29.3", "convex-helpers": "0.1.104", "convex-test": "0.0.38", "cpy-cli": "5.0.0", @@ -271,7 +271,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -558,7 +557,8 @@ "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@convex-dev/rag": { "version": "0.6.0", @@ -634,7 +634,6 @@ "integrity": "sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@edge-runtime/primitives": "6.0.0" }, @@ -1465,6 +1464,7 @@ "integrity": "sha512-ZLAs5YMM5N2UXN3kExMglltJrKKoW7hs3KMZFlXUnD7a5DFKBYxPFMeXA4rT+uvTxuJRZPCYX0JKI5BhyAWx4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", @@ -1487,6 +1487,7 @@ "version": "5.2.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -1498,6 +1499,7 @@ "version": "6.3.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -1634,7 +1636,6 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1862,7 +1863,6 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3050,7 +3050,6 @@ "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3061,7 +3060,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3072,7 +3070,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3082,7 +3079,8 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -3096,7 +3094,8 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", @@ -3144,7 +3143,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -3516,7 +3514,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3557,7 +3554,6 @@ "integrity": "sha512-wmZZfsU40qB77umrcj3YzMSk6cUP5gxLXZDPfiSQLBLegTVXPUdSJC603tR7JB5JkhBDzN5VLaliuRKQGKpUXg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@ai-sdk/gateway": "2.0.3", "@ai-sdk/provider": "2.0.0", @@ -4031,7 +4027,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -4528,6 +4523,7 @@ "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "simple-wcswidth": "^1.1.2" } @@ -4540,12 +4536,11 @@ "license": "MIT" }, "node_modules/convex": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.29.0.tgz", - "integrity": "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.29.3.tgz", + "integrity": "sha512-tg5TXzMjpNk9m50YRtdp6US+t7ckxE4E+7DNKUCjJ2MupQs2RBSPF/z5SNN4GUmQLSfg0eMILDySzdAvjTrhnw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" @@ -4580,7 +4575,6 @@ "integrity": "sha512-7CYvx7T3K6n+McDTK4ZQaQNNGBzq5aWezpjzsKbOxPXx7oNcTP9wrpef3JxeXWFzkByJv5hRCjseh9B7eNJ7Ig==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "convex-helpers": "bin.cjs" }, @@ -5280,7 +5274,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5564,7 +5557,8 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eventsource-parser": { "version": "3.0.6", @@ -6817,7 +6811,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -6961,6 +6954,7 @@ "integrity": "sha512-4cl/KOxq99/c0MtlzXd6rpmOvMUuRHrJTRFzEwz/G+zDygeFm6bbKaa5XRu/VDZs1FsFGKL2WJYNbjFfL2Cg3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", @@ -7862,6 +7856,7 @@ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mustache": "bin/mustache" } @@ -8215,7 +8210,6 @@ "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "openai": "bin/cli" }, @@ -8322,6 +8316,7 @@ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -8377,6 +8372,7 @@ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" @@ -8394,6 +8390,7 @@ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-finally": "^1.0.0" }, @@ -8407,6 +8404,7 @@ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -8685,7 +8683,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8999,7 +8996,6 @@ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9010,7 +9006,6 @@ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9024,7 +9019,6 @@ "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9367,6 +9361,7 @@ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -9730,7 +9725,8 @@ "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/slash": { "version": "4.0.0", @@ -10166,7 +10162,6 @@ "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10335,7 +10330,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10552,7 +10546,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10844,6 +10837,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -10894,7 +10888,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -11011,7 +11004,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11545,7 +11537,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -11569,6 +11560,7 @@ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "dev": true, "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index ad024fa0..3122b05c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ ], "type": "module", "scripts": { + "setup": "node setup.cjs --init", "dev": "run-p -r 'dev:*'", "dev:backend": "convex dev --typecheck-components", "dev:frontend": "cd example && vite --clearScreen false", @@ -109,7 +110,7 @@ "chokidar-cli": "3.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "convex": "1.29.0", + "convex": "1.29.3", "convex-helpers": "0.1.104", "convex-test": "0.0.38", "cpy-cli": "5.0.0", diff --git a/playground/package-lock.json b/playground/package-lock.json index ae29985b..9a37e559 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -92,7 +92,7 @@ }, "..": { "name": "@convex-dev/agent", - "version": "0.2.12", + "version": "0.3.1", "license": "Apache-2.0", "devDependencies": { "@ai-sdk/anthropic": "2.0.39", @@ -100,10 +100,9 @@ "@ai-sdk/openai": "2.0.57", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.14", - "@arethetypeswrong/cli": "0.18.2", - "@convex-dev/rag": "0.5.4", - "@convex-dev/rate-limiter": "0.2.14", - "@convex-dev/workflow": "0.2.8-alpha.10", + "@convex-dev/rag": "0.6.0", + "@convex-dev/rate-limiter": "0.3.0", + "@convex-dev/workflow": "0.3.2", "@edge-runtime/vm": "5.0.0", "@eslint/js": "9.38.0", "@hookform/resolvers": "5.2.2", @@ -125,7 +124,7 @@ "chokidar-cli": "3.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "convex": "1.28.0", + "convex": "1.29.3", "convex-helpers": "0.1.104", "convex-test": "0.0.38", "cpy-cli": "5.0.0", @@ -165,7 +164,7 @@ "peerDependencies": { "@ai-sdk/provider-utils": "^3.0.7", "ai": "^5.0.29", - "convex": "^1.23.0", + "convex": "^1.24.8", "convex-helpers": "^0.1.103", "react": "^18.3.1 || ^19.0.0" }, @@ -2999,7 +2998,6 @@ "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3010,7 +3008,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3021,7 +3018,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3072,7 +3068,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -3304,7 +3299,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3507,7 +3501,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", @@ -3979,8 +3972,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4079,7 +4071,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5141,7 +5132,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5380,7 +5370,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5931,7 +5920,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6048,7 +6036,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6112,7 +6099,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6290,7 +6276,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6382,7 +6367,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/setup.cjs b/setup.cjs index eac0e1b2..6aa9257f 100644 --- a/setup.cjs +++ b/setup.cjs @@ -25,9 +25,10 @@ if (initFlag) { try { console.log("Checking backend configuration..."); - execSync("npm run dev:backend -- --once", { + execSync("npx convex dev --once", { cwd: __dirname, stdio: "inherit", + stderr: "inherit", }); console.log("✅ Backend setup complete! No API key needed.\n"); } catch (error) {