diff --git a/apps/webapp/app/trigger/chat/chat-utils.ts b/apps/webapp/app/trigger/chat/chat-utils.ts new file mode 100644 index 0000000..b0c33b0 --- /dev/null +++ b/apps/webapp/app/trigger/chat/chat-utils.ts @@ -0,0 +1,579 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ActionStatusEnum, LLMMappings } from "@core/types"; +import { logger } from "@trigger.dev/sdk/v3"; +import { + type CoreMessage, + type DataContent, + jsonSchema, + tool, + type ToolSet, +} from "ai"; +import axios from "axios"; +import Handlebars from "handlebars"; + +import { REACT_SYSTEM_PROMPT, REACT_USER_PROMPT } from "./prompt"; +import { generate, processTag } from "./stream-utils"; +import { type AgentMessage, AgentMessageType, Message } from "./types"; +import { type MCP } from "../utils/mcp"; +import { + type ExecutionState, + type HistoryStep, + type Resource, + type TotalCost, +} from "../utils/types"; +import { flattenObject } from "../utils/utils"; +import { searchMemory, addMemory } from "./memory-utils"; + +interface LLMOutputInterface { + response: AsyncGenerator< + | string + | { + type: string; + toolName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any; + toolCallId?: string; + message?: string; + }, + any, + any + >; +} + +const progressUpdateTool = tool({ + description: + "Send a progress update to the user about what has been discovered or will be done next in a crisp and user friendly way no technical terms", + parameters: jsonSchema({ + type: "object", + properties: { + message: { + type: "string", + description: "The progress update message to send to the user", + }, + }, + required: ["message"], + additionalProperties: false, + }), +}); + +const searchMemoryTool = tool({ + description: + "Search the user's memory graph for episodes or statements based on a query", + parameters: jsonSchema({ + type: "object", + properties: { + query: { + type: "string", + description: "The search query to find relevant information in memory", + }, + spaceId: { + type: "string", + description: "Optional space ID to search within a specific space", + }, + sessionId: { + type: "string", + description: "Optional session ID to search within a specific session", + }, + }, + required: ["query"], + additionalProperties: false, + }), +}); + +const addMemoryTool = tool({ + description: "Add information to the user's memory graph", + parameters: jsonSchema({ + type: "object", + properties: { + episodeBody: { + type: "string", + description: "The content/text to add to memory", + }, + referenceTime: { + type: "string", + description: + "ISO 8601 timestamp for when this information is relevant (defaults to current time)", + }, + source: { + type: "string", + description: + "Source of the information (e.g., 'user', 'chat', 'system')", + }, + spaceId: { + type: "string", + description: "Optional space ID to add memory to a specific space", + }, + sessionId: { + type: "string", + description: "Optional session ID to associate with a specific session", + }, + metadata: { + type: "object", + description: "Optional metadata object for additional context", + }, + }, + required: ["episodeBody"], + additionalProperties: false, + }), +}); + +const internalTools = [ + "core--progress_update", + "core--search_memory", + "core--add_memory", +]; + +async function addResources(messages: CoreMessage[], resources: Resource[]) { + const resourcePromises = resources.map(async (resource) => { + // Remove everything before "/api" in the publicURL + if (resource.publicURL) { + const apiIndex = resource.publicURL.indexOf("/api"); + if (apiIndex !== -1) { + resource.publicURL = resource.publicURL.substring(apiIndex); + } + } + const response = await axios.get(resource.publicURL, { + responseType: "arraybuffer", + }); + + if (resource.fileType.startsWith("image/")) { + return { + type: "image", + image: response.data as DataContent, + }; + } + + return { + type: "file", + data: response.data as DataContent, + + mimeType: resource.fileType, + }; + }); + + const content = await Promise.all(resourcePromises); + + return [...messages, { role: "user", content } as CoreMessage]; +} + +function toolToMessage(history: HistoryStep[], messages: CoreMessage[]) { + for (let i = 0; i < history.length; i++) { + const step = history[i]; + + // Add assistant message with tool calls + if (step.observation && step.skillId) { + messages.push({ + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: step.skillId, + toolName: step.skill ?? "", + args: + typeof step.skillInput === "string" + ? JSON.parse(step.skillInput) + : step.skillInput, + }, + ], + }); + + messages.push({ + role: "tool", + content: [ + { + type: "tool-result", + toolName: step.skill, + toolCallId: step.skillId, + result: step.observation, + isError: step.isError, + }, + ], + } as any); + } + // Handle format correction steps (observation exists but no skillId) + else if (step.observation && !step.skillId) { + // Add as a system message for format correction + messages.push({ + role: "system", + content: step.observation, + }); + } + } + + return messages; +} + +async function makeNextCall( + executionState: ExecutionState, + TOOLS: ToolSet, + totalCost: TotalCost, + guardLoop: number, +): Promise { + const { context, history, previousHistory } = executionState; + + const promptInfo = { + USER_MESSAGE: executionState.query, + CONTEXT: context, + USER_MEMORY: executionState.userMemoryContext, + }; + + let messages: CoreMessage[] = []; + + const systemTemplateHandler = Handlebars.compile(REACT_SYSTEM_PROMPT); + let systemPrompt = systemTemplateHandler(promptInfo); + + const userTemplateHandler = Handlebars.compile(REACT_USER_PROMPT); + const userPrompt = userTemplateHandler(promptInfo); + + // Always start with a system message (this does use tokens but keeps the instructions clear) + messages.push({ role: "system", content: systemPrompt }); + + // For subsequent queries, include only final responses from previous exchanges if available + if (previousHistory && previousHistory.length > 0) { + messages = [...messages, ...previousHistory]; + } + + // Add the current user query (much simpler than the full prompt) + messages.push({ role: "user", content: userPrompt }); + + // Include any steps from the current interaction + if (history.length > 0) { + messages = toolToMessage(history, messages); + } + + if (executionState.resources && executionState.resources.length > 0) { + messages = await addResources(messages, executionState.resources); + } + + // Get the next action from the LLM + const response = generate( + messages, + guardLoop > 0 && guardLoop % 3 === 0, + (event) => { + const usage = event.usage; + totalCost.inputTokens += usage.promptTokens; + totalCost.outputTokens += usage.completionTokens; + }, + TOOLS, + ); + + return { response }; +} + +export async function* run( + message: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: Record, + previousHistory: CoreMessage[], + mcp: MCP, + stepHistory: HistoryStep[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): AsyncGenerator { + let guardLoop = 0; + + let tools = { + "core--progress_update": progressUpdateTool, + "core--search_memory": searchMemoryTool, + "core--add_memory": addMemoryTool, + }; + + logger.info("Tools have been formed"); + + let contextText = ""; + let resources = []; + if (context) { + // Extract resources and remove from context + resources = context.resources || []; + delete context.resources; + + // Process remaining context + contextText = flattenObject(context).join("\n"); + } + + const executionState: ExecutionState = { + query: message, + context: contextText, + resources, + previousHistory, + history: stepHistory, // Track the full ReAct history + completed: false, + }; + + const totalCost: TotalCost = { inputTokens: 0, outputTokens: 0, cost: 0 }; + + try { + while (!executionState.completed && guardLoop < 50) { + logger.info(`Starting the loop: ${guardLoop}`); + + const { response: llmResponse } = await makeNextCall( + executionState, + tools, + totalCost, + guardLoop, + ); + + let toolCallInfo; + + const messageState = { + inTag: false, + message: "", + messageEnded: false, + lastSent: "", + }; + + const questionState = { + inTag: false, + message: "", + messageEnded: false, + lastSent: "", + }; + + let totalMessage = ""; + const toolCalls = []; + + // LLM thought response + for await (const chunk of llmResponse) { + if (typeof chunk === "object" && chunk.type === "tool-call") { + toolCallInfo = chunk; + toolCalls.push(chunk); + } + + totalMessage += chunk; + + if (!messageState.messageEnded) { + yield* processTag( + messageState, + totalMessage, + chunk as string, + "", + "", + { + start: AgentMessageType.MESSAGE_START, + chunk: AgentMessageType.MESSAGE_CHUNK, + end: AgentMessageType.MESSAGE_END, + }, + ); + } + + if (!questionState.messageEnded) { + yield* processTag( + questionState, + totalMessage, + chunk as string, + "", + "", + { + start: AgentMessageType.MESSAGE_START, + chunk: AgentMessageType.MESSAGE_CHUNK, + end: AgentMessageType.MESSAGE_END, + }, + ); + } + } + + logger.info(`Cost for thought: ${JSON.stringify(totalCost)}`); + + // Replace the error-handling block with this self-correcting implementation + if ( + !totalMessage.includes("final_response") && + !totalMessage.includes("question_response") && + !toolCallInfo + ) { + // Log the issue for debugging + logger.info( + `Invalid response format detected. Attempting to get proper format.`, + ); + + // Extract the raw content from the invalid response + const rawContent = totalMessage + .replace(/(<[^>]*>|<\/[^>]*>)/g, "") + .trim(); + + // Create a correction step + const stepRecord: HistoryStep = { + thought: "", + skill: "", + skillId: "", + userMessage: "Sol agent error, retrying \n", + isQuestion: false, + isFinal: false, + tokenCount: totalCost, + skillInput: "", + observation: `Your last response was not in a valid format. You must respond with EXACTLY ONE of the required formats: either a tool call, tags, or tags. Please reformat your previous response using the correct format:\n\n${rawContent}`, + }; + + yield Message("", AgentMessageType.MESSAGE_START); + yield Message( + stepRecord.userMessage as string, + AgentMessageType.MESSAGE_CHUNK, + ); + yield Message("", AgentMessageType.MESSAGE_END); + + // Add this step to the history + yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP); + executionState.history.push(stepRecord); + + // Log that we're continuing the loop with a correction request + logger.info(`Added format correction request to history.`); + + // Don't mark as completed - let the loop continue + guardLoop++; // Still increment to prevent infinite loops + continue; + } + + // Record this step in history + const stepRecord: HistoryStep = { + thought: "", + skill: "", + skillId: "", + userMessage: "", + isQuestion: false, + isFinal: false, + tokenCount: totalCost, + skillInput: "", + }; + + if (totalMessage && totalMessage.includes("final_response")) { + executionState.completed = true; + stepRecord.isFinal = true; + stepRecord.userMessage = messageState.message; + stepRecord.finalTokenCount = totalCost; + stepRecord.skillStatus = ActionStatusEnum.SUCCESS; + yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP); + executionState.history.push(stepRecord); + break; + } + + if (totalMessage && totalMessage.includes("question_response")) { + executionState.completed = true; + stepRecord.isQuestion = true; + stepRecord.userMessage = questionState.message; + stepRecord.finalTokenCount = totalCost; + stepRecord.skillStatus = ActionStatusEnum.QUESTION; + yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP); + executionState.history.push(stepRecord); + break; + } + + if (toolCalls && toolCalls.length > 0) { + // Run all tool calls in parallel + for (const toolCallInfo of toolCalls) { + const skillName = toolCallInfo.toolName; + const skillId = toolCallInfo.toolCallId; + const skillInput = toolCallInfo.args; + + const toolName = skillName.split("--")[1]; + const agent = skillName.split("--")[0]; + + const stepRecord: HistoryStep = { + agent, + thought: "", + skill: skillName, + skillId, + userMessage: "", + isQuestion: false, + isFinal: false, + tokenCount: totalCost, + skillInput: JSON.stringify(skillInput), + }; + + if (!internalTools.includes(skillName)) { + const skillMessageToSend = `\n\n`; + + stepRecord.userMessage += skillMessageToSend; + + yield Message("", AgentMessageType.MESSAGE_START); + yield Message(skillMessageToSend, AgentMessageType.MESSAGE_CHUNK); + yield Message("", AgentMessageType.MESSAGE_END); + } + + let result; + try { + // Log skill execution details + logger.info(`Executing skill: ${skillName}`); + logger.info(`Input parameters: ${JSON.stringify(skillInput)}`); + + if (!internalTools.includes(toolName)) { + yield Message( + JSON.stringify({ skillId, status: "start" }), + AgentMessageType.SKILL_START, + ); + } + + // Handle CORE agent tools + if (agent === "core") { + if (toolName === "progress_update") { + yield Message("", AgentMessageType.MESSAGE_START); + yield Message( + skillInput.message, + AgentMessageType.MESSAGE_CHUNK, + ); + stepRecord.userMessage += skillInput.message; + yield Message("", AgentMessageType.MESSAGE_END); + result = "Progress update sent successfully"; + } else if (toolName === "search_memory") { + try { + result = await searchMemory(skillInput); + } catch (apiError) { + logger.error("Memory utils calls failed for search_memory", { + apiError, + }); + result = + "Memory search failed - please check your memory configuration"; + } + } else if (toolName === "add_memory") { + try { + result = await addMemory(skillInput); + } catch (apiError) { + logger.error("Memory utils calls failed for add_memory", { + apiError, + }); + result = + "Memory storage failed - please check your memory configuration"; + } + } + } + // Handle other MCP tools + else { + result = await mcp.callTool(skillName, skillInput); + + yield Message( + JSON.stringify({ result, skillId }), + AgentMessageType.SKILL_CHUNK, + ); + } + + yield Message( + JSON.stringify({ skillId, status: "end" }), + AgentMessageType.SKILL_END, + ); + + stepRecord.skillOutput = + typeof result === "object" + ? JSON.stringify(result, null, 2) + : result; + stepRecord.observation = stepRecord.skillOutput; + } catch (e) { + console.log(e); + logger.error(e as string); + stepRecord.skillInput = skillInput; + stepRecord.observation = JSON.stringify(e); + stepRecord.isError = true; + } + + logger.info(`Skill step: ${JSON.stringify(stepRecord)}`); + + yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP); + executionState.history.push(stepRecord); + } + } + guardLoop++; + } + yield Message("Stream ended", AgentMessageType.STREAM_END); + } catch (e) { + logger.error(e as string); + yield Message((e as Error).message, AgentMessageType.ERROR); + yield Message("Stream ended", AgentMessageType.STREAM_END); + } +} diff --git a/apps/webapp/app/trigger/chat/chat.ts b/apps/webapp/app/trigger/chat/chat.ts new file mode 100644 index 0000000..738a1d4 --- /dev/null +++ b/apps/webapp/app/trigger/chat/chat.ts @@ -0,0 +1,131 @@ +import { PrismaClient } from "@prisma/client"; +import { ActionStatusEnum } from "@core/types"; +import { logger, metadata, task } from "@trigger.dev/sdk/v3"; +import { format } from "date-fns"; + +import { run } from "./chat-utils"; +import { MCP } from "../utils/mcp"; +import { type HistoryStep } from "../utils/types"; +import { + createConversationHistoryForAgent, + getPreviousExecutionHistory, + init, + type RunChatPayload, + updateConversationHistoryMessage, + updateConversationStatus, + updateExecutionStep, +} from "../utils/utils"; + +const prisma = new PrismaClient(); + +/** + * Main chat task that orchestrates the agent workflow + * Handles conversation context, agent selection, and LLM interactions + */ +export const chat = task({ + id: "chat", + maxDuration: 3000, + queue: { + name: "chat", + concurrencyLimit: 30, + }, + init, + run: async (payload: RunChatPayload, { init }) => { + await updateConversationStatus("running", payload.conversationId); + + try { + let creditForChat = 0; + + const { previousHistory, ...otherData } = payload.context; + + const isContinuation = payload.isContinuation || false; + + // Initialise mcp + const mcp = new MCP(); + await mcp.init(); + + // Prepare context with additional metadata + const context = { + // Currently this is assuming we only have one page in context + context: { + ...(otherData.page && otherData.page.length > 0 + ? { page: otherData.page[0] } + : {}), + }, + workpsaceId: init?.conversation.workspaceId, + resources: otherData.resources, + }; + + // Extract user's goal from conversation history + const message = init?.conversationHistory?.message; + // Retrieve execution history from previous interactions + const previousExecutionHistory = getPreviousExecutionHistory( + previousHistory ?? [], + ); + + let agentUserMessage = ""; + let agentConversationHistory; + let stepHistory: HistoryStep[] = []; + // Prepare conversation history in agent-compatible format + agentConversationHistory = await createConversationHistoryForAgent( + payload.conversationId, + ); + + const llmResponse = run( + message as string, + context, + previousExecutionHistory, + mcp, + stepHistory, + ); + + const stream = await metadata.stream("messages", llmResponse); + + let conversationStatus = "success"; + for await (const step of stream) { + if (step.type === "STEP") { + creditForChat += 1; + const stepDetails = JSON.parse(step.message as string); + + if (stepDetails.skillStatus === ActionStatusEnum.TOOL_REQUEST) { + conversationStatus = "need_approval"; + } + + if (stepDetails.skillStatus === ActionStatusEnum.QUESTION) { + conversationStatus = "need_attention"; + } + + await updateExecutionStep( + { ...stepDetails }, + agentConversationHistory.id, + ); + + agentUserMessage += stepDetails.userMessage; + + await updateConversationHistoryMessage( + agentUserMessage, + agentConversationHistory.id, + ); + } else if (step.type === "STREAM_END") { + break; + } + } + + await updateConversationStatus( + conversationStatus, + payload.conversationId, + ); + + // await addToMemory( + // init.conversation.id, + // message, + // agentUserMessage, + // init.preferences, + // init.userName, + // ); + } catch (e) { + await updateConversationStatus("failed", payload.conversationId); + throw new Error(e as string); + } + }, +}); diff --git a/apps/webapp/app/trigger/chat/memory-utils.ts b/apps/webapp/app/trigger/chat/memory-utils.ts new file mode 100644 index 0000000..8d08eac --- /dev/null +++ b/apps/webapp/app/trigger/chat/memory-utils.ts @@ -0,0 +1,48 @@ +import { logger } from "@trigger.dev/sdk/v3"; +import axios from "axios"; + +// Memory API functions using axios interceptor +export interface SearchMemoryParams { + query: string; + spaceId?: string; + sessionId?: string; +} + +export interface AddMemoryParams { + episodeBody: string; + referenceTime?: string; + source?: string; + spaceId?: string; + sessionId?: string; + metadata?: any; +} + +export const searchMemory = async (params: SearchMemoryParams) => { + try { + const response = await axios.post("https://core::memory/search", params); + return response.data; + } catch (error) { + logger.error("Memory search failed", { error, params }); + return { error: "Memory search failed" }; + } +}; + +export const addMemory = async (params: AddMemoryParams) => { + try { + // Set defaults for required fields + const memoryInput = { + ...params, + referenceTime: params.referenceTime || new Date().toISOString(), + source: params.source || "chat", + }; + + const response = await axios.post( + "https://core::memory/ingest", + memoryInput, + ); + return response.data; + } catch (error) { + logger.error("Memory storage failed", { error, params }); + return { error: "Memory storage failed" }; + } +}; diff --git a/apps/webapp/app/trigger/chat/prompt.ts b/apps/webapp/app/trigger/chat/prompt.ts new file mode 100644 index 0000000..75e694b --- /dev/null +++ b/apps/webapp/app/trigger/chat/prompt.ts @@ -0,0 +1,131 @@ +export const REACT_SYSTEM_PROMPT = ` +You are a helpful AI assistant with access to user memory. Your primary capabilities are: + +1. **Memory-First Approach**: Always check user memory first to understand context and previous interactions +2. **Memory Management**: Help users store, retrieve, and organize information in their memory +3. **Contextual Assistance**: Use memory to provide personalized and contextual responses + + +{{CONTEXT}} + + + +- Always check memory FIRST using core--search_memory before any other actions +- Consider this your highest priority for EVERY interaction - as essential as breathing +- Make memory checking your first tool call before any other operations + +QUERY FORMATION: +- Write specific factual statements as queries (e.g., "user email address" not "what is the user's email?") +- Create multiple targeted memory queries for complex requests + +KEY QUERY AREAS: +- Personal context: user name, location, identity, work context +- Project context: repositories, codebases, current work, team members +- Task context: recent tasks, ongoing projects, deadlines, priorities +- Integration context: GitHub repos, Slack channels, Linear projects, connected services +- Communication patterns: email preferences, notification settings, workflow automation +- Technical context: coding languages, frameworks, development environment +- Collaboration context: team members, project stakeholders, meeting patterns +- Preferences: likes, dislikes, communication style, tool preferences +- History: previous discussions, past requests, completed work, recurring issues +- Automation rules: user-defined workflows, triggers, automation preferences + +MEMORY USAGE: +- Execute multiple memory queries in parallel rather than sequentially +- Batch related memory queries when possible +- Prioritize recent information over older memories +- Create comprehensive context-aware queries based on user message/activity content +- Extract and query SEMANTIC CONTENT, not just structural metadata +- Parse titles, descriptions, and content for actual subject matter keywords +- Search internal SOL tasks/conversations that may relate to the same topics +- Query ALL relatable concepts, not just direct keywords or IDs +- Search for similar past situations, patterns, and related work +- Include synonyms, related terms, and contextual concepts in queries +- Query user's historical approach to similar requests or activities +- Search for connected projects, tasks, conversations, and collaborations +- Retrieve workflow patterns and past decision-making context +- Query broader domain context beyond immediate request scope +- Remember: SOL tracks work that external tools don't - search internal content thoroughly +- Blend memory insights naturally into responses +- Verify you've checked relevant memory before finalizing ANY response + +If memory access is unavailable, rely only on the current conversation or ask user + + + +You have tools at your disposal to assist users: + +CORE PRINCIPLES: +- Use tools only when necessary for the task at hand +- Always check memory FIRST before making other tool calls +- Execute multiple operations in parallel whenever possible +- Use sequential calls only when output of one is required for input of another + +PARAMETER HANDLING: +- Follow tool schemas exactly with all required parameters +- Only use values that are: + • Explicitly provided by the user (use EXACTLY as given) + • Reasonably inferred from context + • Retrieved from memory or prior tool calls +- Never make up values for required parameters +- Omit optional parameters unless clearly needed +- Analyze user's descriptive terms for parameter clues + +TOOL SELECTION: +- Never call tools not provided in this conversation +- Skip tool calls for general questions you can answer directly +- For identical operations on multiple items, use parallel tool calls +- Default to parallel execution (3-5× faster than sequential calls) +- You can always access external service tools by loading them with load_mcp first + +TOOL MENTION HANDLING: +When user message contains : +- Extract tool_name from data-id attribute +- First check if it's a built-in tool; if not, check EXTERNAL SERVICES TOOLS +- If available: Load it with load_mcp and focus on addressing the request with this tool +- If unavailable: Inform user and suggest alternatives if possible +- For multiple tool mentions: Load all applicable tools in a single load_mcp call + +ERROR HANDLING: +- If a tool returns an error, try fixing parameters before retrying +- If you can't resolve an error, explain the issue to the user +- Consider alternative tools when primary tools are unavailable + + + +Use EXACTLY ONE of these formats for all user-facing communication: + +PROGRESS UPDATES - During processing: +- Use the core--progress_update tool to keep users informed +- Update users about what you're discovering or doing next +- Keep messages clear and user-friendly +- Avoid technical jargon + +QUESTIONS - When you need information: + +

[Your question with HTML formatting]

+
+ +- Ask questions only when you cannot find information through memory or tools +- Be specific about what you need to know +- Provide context for why you're asking + +FINAL ANSWERS - When completing tasks: + +

[Your answer with HTML formatting]

+
+ +CRITICAL: +- Use ONE format per turn +- Apply proper HTML formatting (

,

,

,

    ,
  • , etc.) +- Never mix communication formats +- Keep responses clear and helpful + +`; + +export const REACT_USER_PROMPT = ` +Here is the user message: + +{{USER_MESSAGE}} + +`; diff --git a/apps/webapp/app/trigger/chat/stream-utils.ts b/apps/webapp/app/trigger/chat/stream-utils.ts new file mode 100644 index 0000000..94be24a --- /dev/null +++ b/apps/webapp/app/trigger/chat/stream-utils.ts @@ -0,0 +1,263 @@ +import fs from "fs"; +import path from "node:path"; + +import { anthropic } from "@ai-sdk/anthropic"; +import { google } from "@ai-sdk/google"; +import { openai } from "@ai-sdk/openai"; +import { logger } from "@trigger.dev/sdk/v3"; +import { + type CoreMessage, + type LanguageModelV1, + streamText, + type ToolSet, +} from "ai"; +import { createOllama } from "ollama-ai-provider"; + +import { type AgentMessageType, Message } from "./types"; + +interface State { + inTag: boolean; + messageEnded: boolean; + message: string; + lastSent: string; +} + +export interface ExecutionState { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + agentFlow: any; + userMessage: string; + message: string; +} + +export async function* processTag( + state: State, + totalMessage: string, + chunk: string, + startTag: string, + endTag: string, + states: { start: string; chunk: string; end: string }, + extraParams: Record = {}, +) { + let comingFromStart = false; + + if (!state.messageEnded) { + if (!state.inTag) { + const startIndex = totalMessage.indexOf(startTag); + if (startIndex !== -1) { + state.inTag = true; + // Send MESSAGE_START when we first enter the tag + yield Message("", states.start as AgentMessageType, extraParams); + const chunkToSend = totalMessage.slice(startIndex + startTag.length); + state.message += chunkToSend; + comingFromStart = true; + } + } + + if (state.inTag) { + // Check if chunk contains end tag + const hasEndTag = chunk.includes(endTag); + const hasStartTag = chunk.includes(startTag); + const hasClosingTag = chunk.includes(" void, + tools?: ToolSet, + system?: string, + model?: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): AsyncGenerator< + | string + | { + type: string; + toolName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any; + toolCallId?: string; + message?: string; + } +> { + // Check for API keys + const anthropicKey = process.env.ANTHROPIC_API_KEY; + const googleKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY; + const openaiKey = process.env.OPENAI_API_KEY; + const ollamaUrl = process.env.OLLAMA_URL; + model = model || process.env.MODEL; + + let modelInstance; + let modelTemperature = Number(process.env.MODEL_TEMPERATURE) || 1; + + // First check if Ollama URL exists and use Ollama + if (ollamaUrl) { + const ollama = createOllama({ + baseURL: ollamaUrl, + }); + modelInstance = ollama(model || "llama2"); // Default to llama2 if no model specified + } else { + // If no Ollama, check other models + switch (model) { + case "claude-3-7-sonnet-20250219": + case "claude-3-opus-20240229": + case "claude-3-5-haiku-20241022": + if (!anthropicKey) { + throw new Error("No Anthropic API key found. Set ANTHROPIC_API_KEY"); + } + modelInstance = anthropic(model); + modelTemperature = 0.5; + break; + + case "gemini-2.5-flash-preview-04-17": + case "gemini-2.5-pro-preview-03-25": + case "gemini-2.0-flash": + case "gemini-2.0-flash-lite": + if (!googleKey) { + throw new Error("No Google API key found. Set GOOGLE_API_KEY"); + } + modelInstance = google(model); + break; + + case "gpt-4.1-2025-04-14": + case "gpt-4.1-mini-2025-04-14": + case "gpt-4.1-nano-2025-04-14": + if (!openaiKey) { + throw new Error("No OpenAI API key found. Set OPENAI_API_KEY"); + } + modelInstance = openai(model); + break; + + default: + break; + } + } + + logger.info("starting stream"); + // Try Anthropic next if key exists + if (modelInstance) { + try { + const { textStream, fullStream } = streamText({ + model: modelInstance as LanguageModelV1, + messages, + temperature: modelTemperature, + maxSteps: 10, + tools, + ...(isProgressUpdate + ? { toolChoice: { type: "tool", toolName: "core--progress_update" } } + : {}), + toolCallStreaming: true, + onFinish, + ...(system ? { system } : {}), + }); + + for await (const chunk of textStream) { + yield chunk; + } + + for await (const fullChunk of fullStream) { + if (fullChunk.type === "tool-call") { + yield { + type: "tool-call", + toolName: fullChunk.toolName, + toolCallId: fullChunk.toolCallId, + args: fullChunk.args, + }; + } + + if (fullChunk.type === "error") { + // Log the error to a file + const errorLogsDir = path.join(__dirname, "../../../../logs/errors"); + + // Ensure the directory exists + try { + if (!fs.existsSync(errorLogsDir)) { + fs.mkdirSync(errorLogsDir, { recursive: true }); + } + + // Create a timestamped error log file + const timestamp = new Date().toISOString().replace(/:/g, "-"); + const errorLogPath = path.join( + errorLogsDir, + `llm-error-${timestamp}.json`, + ); + + // Write the error to the file + fs.writeFileSync( + errorLogPath, + JSON.stringify({ + timestamp: new Date().toISOString(), + error: fullChunk.error, + }), + ); + + logger.error(`LLM error logged to ${errorLogPath}`); + } catch (err) { + logger.error(`Failed to log LLM error: ${err}`); + } + } + } + return; + } catch (e) { + console.log(e); + logger.error(e as string); + } + } + + throw new Error("No valid LLM configuration found"); +} diff --git a/apps/webapp/app/trigger/chat/types.ts b/apps/webapp/app/trigger/chat/types.ts new file mode 100644 index 0000000..61c2342 --- /dev/null +++ b/apps/webapp/app/trigger/chat/types.ts @@ -0,0 +1,46 @@ +export interface AgentStep { + agent: string; + goal: string; + reasoning: string; +} + +export enum AgentMessageType { + STREAM_START = 'STREAM_START', + STREAM_END = 'STREAM_END', + + // Used in ReACT based prompting + THOUGHT_START = 'THOUGHT_START', + THOUGHT_CHUNK = 'THOUGHT_CHUNK', + THOUGHT_END = 'THOUGHT_END', + + // Message types + MESSAGE_START = 'MESSAGE_START', + MESSAGE_CHUNK = 'MESSAGE_CHUNK', + MESSAGE_END = 'MESSAGE_END', + + // This is used to return action input + SKILL_START = 'SKILL_START', + SKILL_CHUNK = 'SKILL_CHUNK', + SKILL_END = 'SKILL_END', + + STEP = 'STEP', + ERROR = 'ERROR', +} + +export interface AgentMessage { + message?: string; + type: AgentMessageType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata: Record; +} + +export const Message = ( + message: string, + type: AgentMessageType, + extraParams: Record = {}, +): AgentMessage => { + // For all message types, we use the message field + // The type field differentiates how the message should be interpreted + // For STEP and SKILL types, the message can contain JSON data as a string + return { message, type, metadata: extraParams }; +}; diff --git a/apps/webapp/app/trigger/utils/mcp.ts b/apps/webapp/app/trigger/utils/mcp.ts new file mode 100644 index 0000000..85d7ab4 --- /dev/null +++ b/apps/webapp/app/trigger/utils/mcp.ts @@ -0,0 +1,151 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { logger } from "@trigger.dev/sdk/v3"; +import { jsonSchema, tool, type ToolSet } from "ai"; + +import { type MCPTool } from "./types"; + +export class MCP { + private Client: any; + private clients: Record = {}; + private StdioTransport: any; + + constructor() {} + + public async init() { + this.Client = await MCP.importClient(); + this.StdioTransport = await MCP.importStdioTransport(); + } + + private static async importClient() { + const { Client } = await import( + "@modelcontextprotocol/sdk/client/index.js" + ); + return Client; + } + + async load(agents: string[], mcpConfig: any) { + await Promise.all( + agents.map(async (agent) => { + const mcp = mcpConfig.mcpServers[agent]; + + return await this.connectToServer(agent, mcp.command, mcp.args, { + ...mcp.env, + DATABASE_URL: mcp.env?.DATABASE_URL ?? "", + }); + }), + ); + } + + private static async importStdioTransport() { + const { StdioClientTransport } = await import("./stdio"); + return StdioClientTransport; + } + + async allTools(): Promise { + const clientEntries = Object.entries(this.clients); + + // Fetch all tools in parallel + const toolsArrays = await Promise.all( + clientEntries.map(async ([clientKey, client]) => { + try { + const { tools } = await client.listTools(); + return tools.map(({ name, description, inputSchema }: any) => [ + `${clientKey}--${name}`, + tool({ + description, + parameters: jsonSchema(inputSchema), + }), + ]); + } catch (error) { + logger.error(`Error fetching tools for ${clientKey}:`, { error }); + return []; + } + }), + ); + + // Flatten and convert to object + return Object.fromEntries(toolsArrays.flat()); + } + + async tools(): Promise { + const allTools: MCPTool[] = []; + + for (const clientKey in this.clients) { + const client = this.clients[clientKey]; + const { tools: clientTools } = await client.listTools(); + + for (const tool of clientTools) { + // Add client prefix to tool name + tool.name = `${clientKey}--${tool.name}`; + allTools.push(tool); + } + } + + return allTools; + } + + async getTool(name: string) { + try { + const clientKey = name.split("--")[0]; + const toolName = name.split("--")[1]; + const client = this.clients[clientKey]; + const { tools: clientTools } = await client.listTools(); + const clientTool = clientTools.find((to: any) => to.name === toolName); + + return JSON.stringify(clientTool); + } catch (e) { + logger.error((e as string) ?? "Getting tool failed"); + throw new Error("Getting tool failed"); + } + } + + async callTool(name: string, parameters: any) { + const clientKey = name.split("--")[0]; + const toolName = name.split("--")[1]; + + const client = this.clients[clientKey]; + + const response = await client.callTool({ + name: toolName, + arguments: parameters, + }); + + return response; + } + + async connectToServer( + name: string, + command: string, + args: string[], + env: any, + ) { + try { + const client = new this.Client( + { + name, + version: "1.0.0", + }, + { + capabilities: {}, + }, + ); + + // Conf + // igure the transport for MCP server + const transport = new this.StdioTransport({ + command, + args, + env, + }); + + // Connect to the MCP server + await client.connect(transport, { timeout: 60 * 1000 * 5 }); + this.clients[name] = client; + + logger.info(`Connected to ${name} MCP server`); + } catch (e) { + logger.error(`Failed to connect to ${name} MCP server: `, { e }); + throw e; + } + } +} diff --git a/apps/webapp/app/trigger/utils/stdio.ts b/apps/webapp/app/trigger/utils/stdio.ts new file mode 100644 index 0000000..bf4db1f --- /dev/null +++ b/apps/webapp/app/trigger/utils/stdio.ts @@ -0,0 +1,256 @@ +import { type ChildProcess, type IOType } from "node:child_process"; +import process from "node:process"; +import { type Stream } from "node:stream"; + +import { type Transport } from "@modelcontextprotocol/sdk/shared/transport"; +import { + type JSONRPCMessage, + JSONRPCMessageSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { execa } from "execa"; + +/** + * Buffers a continuous stdio stream into discrete JSON-RPC messages. + */ +export class ReadBuffer { + private _buffer?: Buffer; + + append(chunk: Buffer): void { + this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; + } + + readMessage(): JSONRPCMessage | null { + if (!this._buffer) { + return null; + } + + const index = this._buffer.indexOf("\n"); + if (index === -1) { + return null; + } + + const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); + this._buffer = this._buffer.subarray(index + 1); + return deserializeMessage(line); + } + + clear(): void { + this._buffer = undefined; + } +} + +export function deserializeMessage(line: string): JSONRPCMessage { + return JSONRPCMessageSchema.parse(JSON.parse(line)); +} + +export function serializeMessage(message: JSONRPCMessage): string { + return `${JSON.stringify(message)}\n`; +} + +export interface StdioServerParameters { + /** + * The executable to run to start the server. + */ + command: string; + + /** + * Command line arguments to pass to the executable. + */ + args?: string[]; + + /** + * The environment to use when spawning the process. + * + * If not specified, the result of getDefaultEnvironment() will be used. + */ + env?: Record; + + /** + * How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`. + * + * The default is "inherit", meaning messages to stderr will be printed to the parent process's stderr. + */ + stderr?: IOType | Stream | number; + + /** + * The working directory to use when spawning the process. + * + * If not specified, the current working directory will be inherited. + */ + cwd?: string; +} + +/** + * Environment variables to inherit by default, if an environment is not explicitly given. + */ +export const DEFAULT_INHERITED_ENV_VARS = + process.platform === "win32" + ? [ + "APPDATA", + "HOMEDRIVE", + "HOMEPATH", + "LOCALAPPDATA", + "PATH", + "PROCESSOR_ARCHITECTURE", + "SYSTEMDRIVE", + "SYSTEMROOT", + "TEMP", + "USERNAME", + "USERPROFILE", + ] + : /* list inspired by the default env inheritance of sudo */ + ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"]; + +/** + * Returns a default environment object including only environment variables deemed safe to inherit. + */ +export function getDefaultEnvironment(): Record { + const env: Record = {}; + + for (const key of DEFAULT_INHERITED_ENV_VARS) { + const value = process.env[key]; + if (value === undefined) { + continue; + } + + if (value.startsWith("()")) { + // Skip functions, which are a security risk. + continue; + } + + env[key] = value; + } + + return env; +} + +/** + * Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout. + * + * This transport is only available in Node.js environments. + */ +export class StdioClientTransport implements Transport { + private _process?: ChildProcess; + private _abortController: AbortController = new AbortController(); + private _readBuffer: ReadBuffer = new ReadBuffer(); + private _serverParams: StdioServerParameters; + + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: JSONRPCMessage) => void; + + constructor(server: StdioServerParameters) { + this._serverParams = server; + } + + /** + * Starts the server process and prepares to communicate with it. + */ + async start(): Promise { + if (this._process) { + throw new Error( + "StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.", + ); + } + + return new Promise((resolve, reject) => { + this._process = execa( + this._serverParams.command, + this._serverParams.args ?? [], + { + env: this._serverParams.env ?? getDefaultEnvironment(), + stderr: "inherit", + shell: "/bin/sh", + windowsHide: process.platform === "win32" && isElectron(), + cwd: this._serverParams.cwd, + cancelSignal: this._abortController.signal, + stdin: "pipe", + stdout: "pipe", + }, + ); + + this._process.on("error", (error) => { + if (error.name === "AbortError") { + // Expected when close() is called. + this.onclose?.(); + return; + } + + reject(error); + this.onerror?.(error); + }); + + this._process.on("spawn", () => { + resolve(); + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + this._process.on("close", (_code) => { + this._process = undefined; + this.onclose?.(); + }); + + this._process.stdin?.on("error", (error) => { + this.onerror?.(error); + }); + + this._process.stdout?.on("data", (chunk) => { + this._readBuffer.append(chunk); + this.processReadBuffer(); + }); + + this._process.stdout?.on("error", (error) => { + this.onerror?.(error); + }); + }); + } + + /** + * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped". + * + * This is only available after the process has been started. + */ + get stderr(): Stream | null { + return this._process?.stderr ?? null; + } + + private processReadBuffer() { + while (true) { + try { + const message = this._readBuffer.readMessage(); + if (message === null) { + break; + } + + this.onmessage?.(message); + } catch (error) { + this.onerror?.(error as Error); + } + } + } + + async close(): Promise { + this._abortController.abort(); + this._process = undefined; + this._readBuffer.clear(); + } + + send(message: JSONRPCMessage): Promise { + return new Promise((resolve) => { + if (!this._process?.stdin) { + throw new Error("Not connected"); + } + + const json = serializeMessage(message); + if (this._process.stdin.write(json)) { + resolve(); + } else { + this._process.stdin.once("drain", resolve); + } + }); + } +} + +function isElectron() { + return "type" in process; +} diff --git a/apps/webapp/app/trigger/utils/types.ts b/apps/webapp/app/trigger/utils/types.ts new file mode 100644 index 0000000..2f3788a --- /dev/null +++ b/apps/webapp/app/trigger/utils/types.ts @@ -0,0 +1,123 @@ +import { type ActionStatusEnum } from "@core/types"; +import { type CoreMessage } from "ai"; + +// Define types for the MCP tool schema +export interface MCPTool { + name: string; + description: string; + inputSchema: { + type: string; + properties: Record; + required?: string[]; + additionalProperties: boolean; + $schema: string; + }; +} + +// Vercel AI SDK Tool Types +export type VercelAITools = Record< + string, + { + type: "function"; + description: string; + parameters: { + type: "object"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties: Record; + required?: string[]; + }; + } +>; + +export type SchemaProperty = + | { + type: string | string[]; + minimum?: number; + maximum?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default?: any; + minLength?: number; + pattern?: string; + enum?: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + items?: any; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + description?: string; + } + | { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + anyOf: any[]; + }; + +export interface Resource { + id?: string; + size?: number; + fileType: string; + publicURL: string; + originalName?: string; +} + +export interface ExecutionState { + query: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context?: string; + resources: Resource[]; + previousHistory?: CoreMessage[]; + history: HistoryStep[]; + userMemoryContext?: string; + automationContext?: string; + completed: boolean; +} + +export interface TokenCount { + inputTokens: number; + outputToken: number; +} + +export interface TotalCost { + inputTokens: number; + outputTokens: number; + cost: number; +} + +export interface HistoryStep { + agent?: string; + + // The agent's reasoning process for this step + thought?: string; + + // Indicates if this step contains a question for the user + isQuestion?: boolean; + // Indicates if this is the final response in the conversation + isFinal?: boolean; + isError?: boolean; + + // The name of the skill/tool being used in this step + skill?: string; + skillId?: string; + skillInput?: string; + skillOutput?: string; + skillStatus?: ActionStatusEnum; + + // This is when the action has run and the output will be put here + observation?: string; + + // This is what the user will read + userMessage?: string; + + // If the agent has run completely + completed?: boolean; + + // Token count + tokenCount: TotalCost; + + finalTokenCount?: TotalCost; +} + +export interface GenerateResponse { + text: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toolCalls: any[]; +} diff --git a/apps/webapp/app/trigger/utils/utils.ts b/apps/webapp/app/trigger/utils/utils.ts new file mode 100644 index 0000000..31d5801 --- /dev/null +++ b/apps/webapp/app/trigger/utils/utils.ts @@ -0,0 +1,432 @@ +import { + type Activity, + type Conversation, + type ConversationHistory, + type IntegrationDefinitionV2, + type Prisma, + PrismaClient, + UserType, + type Workspace, +} from "@prisma/client"; + +import { logger } from "@trigger.dev/sdk/v3"; +import { type CoreMessage } from "ai"; + +import { type HistoryStep } from "./types"; +import axios from "axios"; + +const prisma = new PrismaClient(); + +export interface InitChatPayload { + conversationId: string; + conversationHistoryId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any; + pat: string; +} + +export class Preferences { + timezone?: string; + + // Memory details + memory_host?: string; + memory_api_key?: string; +} + +export interface RunChatPayload { + conversationId: string; + conversationHistoryId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any; + conversation: Conversation; + conversationHistory: ConversationHistory; + pat: string; + isContinuation?: boolean; +} + +export const init = async (payload: InitChatPayload) => { + logger.info("Loading init"); + const conversationHistory = await prisma.conversationHistory.findUnique({ + where: { id: payload.conversationHistoryId }, + include: { conversation: true }, + }); + + const conversation = conversationHistory?.conversation as Conversation; + + const workspace = await prisma.workspace.findUnique({ + where: { id: conversation.workspaceId as string }, + }); + + if (!workspace) { + return { conversation, conversationHistory }; + } + + const pat = await prisma.personalAccessToken.findFirst({ + where: { userId: workspace.userId as string, name: "default" }, + }); + + const user = await prisma.user.findFirst({ + where: { id: workspace.userId as string }, + }); + + const integrationAccounts = await prisma.integrationAccount.findMany({ + where: { + workspaceId: workspace.id, + }, + include: { integrationDefinition: true }, + }); + + // Create MCP server configurations for each integration account + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const integrationMCPServers: Record = {}; + + for (const account of integrationAccounts) { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spec = account.integrationDefinition?.spec as any; + if (spec.mcp) { + const mcpSpec = spec.mcp; + const configuredMCP = { ...mcpSpec }; + + // Replace config placeholders in environment variables + if (configuredMCP.env) { + for (const [key, value] of Object.entries(configuredMCP.env)) { + if (typeof value === "string" && value.includes("${config:")) { + // Extract the config key from the placeholder + const configKey = value.match(/\$\{config:(.*?)\}/)?.[1]; + if ( + configKey && + account.integrationConfiguration && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationConfiguration as any)[configKey] + ) { + configuredMCP.env[key] = value.replace( + `\${config:${configKey}}`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationConfiguration as any)[configKey], + ); + } + } + + if ( + typeof value === "string" && + value.includes("${integrationConfig:") + ) { + // Extract the config key from the placeholder + const configKey = value.match( + /\$\{integrationConfig:(.*?)\}/, + )?.[1]; + if ( + configKey && + account.integrationDefinition.config && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationDefinition.config as any)[configKey] + ) { + configuredMCP.env[key] = value.replace( + `\${integrationConfig:${configKey}}`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationDefinition.config as any)[configKey], + ); + } + } + } + } + + // Add to the MCP servers collection + integrationMCPServers[account.integrationDefinition.slug] = + configuredMCP; + } + + axios.interceptors.request.use((config) => { + if (config.url?.startsWith("https://core::memory")) { + // Handle both search and ingest endpoints + if (config.url.includes("/search")) { + config.url = `${process.env.API_BASE_URL}/search`; + } else if (config.url.includes("/ingest")) { + config.url = `${process.env.API_BASE_URL}/ingest`; + } + config.headers.Authorization = `Bearer ${payload.pat}`; + } + + return config; + }); + } catch (error) { + logger.error( + `Failed to configure MCP for ${account.integrationDefinition?.slug}:`, + { error }, + ); + } + } + + return { + conversation, + conversationHistory, + token: pat?.obfuscatedToken, + userId: user?.id, + userName: user?.name, + }; +}; + +export const createConversationHistoryForAgent = async ( + conversationId: string, +) => { + return await prisma.conversationHistory.create({ + data: { + conversationId, + message: "Generating...", + userType: "Agent", + thoughts: {}, + }, + }); +}; + +export const getConversationHistoryFormat = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + previousHistory: any[], +): string => { + if (previousHistory) { + const historyText = previousHistory + .map((history) => `${history.userType}: \n ${history.message}`) + .join("\n------------\n"); + + return historyText; + } + + return ""; +}; + +export const getPreviousExecutionHistory = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + previousHistory: any[], +): CoreMessage[] => { + return previousHistory.map((history) => ({ + role: history.userType === "User" ? "user" : "assistant", + content: history.message, + })); +}; + +export const getIntegrationDefinitionsForAgents = (agents: string[]) => { + return prisma.integrationDefinitionV2.findMany({ + where: { + slug: { + in: agents, + }, + }, + }); +}; + +export const getIntegrationConfigForIntegrationDefinition = ( + integrationDefinitionId: string, +) => { + return prisma.integrationAccount.findFirst({ + where: { + integrationDefinitionId, + }, + }); +}; + +export const updateExecutionStep = async ( + step: HistoryStep, + conversationHistoryId: string, +) => { + const { + thought, + userMessage, + skillInput, + skillOutput, + skillId, + skillStatus, + ...metadata + } = step; + + await prisma.conversationExecutionStep.create({ + data: { + thought: thought ?? "", + message: userMessage ?? "", + actionInput: + typeof skillInput === "object" + ? JSON.stringify(skillInput) + : skillInput, + actionOutput: + typeof skillOutput === "object" + ? JSON.stringify(skillOutput) + : skillOutput, + actionId: skillId, + actionStatus: skillStatus, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata: metadata as any, + conversationHistoryId, + }, + }); +}; + +export const updateConversationHistoryMessage = async ( + userMessage: string, + conversationHistoryId: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thoughts?: Record, +) => { + await prisma.conversationHistory.update({ + where: { + id: conversationHistoryId, + }, + data: { + message: userMessage, + thoughts, + userType: UserType.Agent, + }, + }); +}; + +export const getExecutionStepsForConversation = async ( + conversationHistoryId: string, +) => { + const lastExecutionSteps = await prisma.conversationExecutionStep.findMany({ + where: { + conversationHistoryId, + }, + }); + + return lastExecutionSteps; +}; + +export const getActivityDetails = async (activityId: string) => { + if (!activityId) { + return {}; + } + + const activity = await prisma.activity.findFirst({ + where: { + id: activityId, + }, + }); + + return { + activityId, + integrationAccountId: activity?.integrationAccountId, + sourceURL: activity?.sourceURL, + }; +}; + +/** + * Generates a random ID of 6 characters + * @returns A random string of 6 characters + */ +export const generateRandomId = (): string => { + // Define characters that can be used in the ID + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + + // Generate 6 random characters + for (let i = 0; i < 6; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters.charAt(randomIndex); + } + + return result.toLowerCase(); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function flattenObject(obj: Record, prefix = ""): string[] { + return Object.entries(obj).reduce((result, [key, value]) => { + const entryKey = prefix ? `${prefix}_${key}` : key; + + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + // For nested objects, flatten them and add to results + return [...result, ...flattenObject(value, entryKey)]; + } + + // For primitive values or arrays, add directly + return [...result, `- ${entryKey}: ${value}`]; + }, []); +} + +export const updateConversationStatus = async ( + status: string, + conversationId: string, +) => { + const data: Prisma.ConversationUpdateInput = { status, unread: true }; + + return await prisma.conversation.update({ + where: { + id: conversationId, + }, + data, + }); +}; + +export const getActivity = async (activityId: string) => { + return await prisma.activity.findUnique({ + where: { + id: activityId, + }, + include: { + workspace: true, + integrationAccount: { + include: { + integrationDefinition: true, + }, + }, + }, + }); +}; + +export const updateActivity = async ( + activityId: string, + rejectionReason: string, +) => { + return await prisma.activity.update({ + where: { + id: activityId, + }, + data: { + rejectionReason, + }, + }); +}; + +export const createConversation = async ( + activity: Activity, + workspace: Workspace, + integrationDefinition: IntegrationDefinitionV2, + automationContext: { automations?: string[]; executionPlan: string }, +) => { + const conversation = await prisma.conversation.create({ + data: { + workspaceId: activity.workspaceId, + userId: workspace.userId as string, + title: activity.text.substring(0, 100), + ConversationHistory: { + create: { + userId: workspace.userId, + message: `Activity from ${integrationDefinition.name} \n Content: ${activity.text}`, + userType: UserType.User, + activityId: activity.id, + thoughts: { ...automationContext }, + }, + }, + }, + include: { + ConversationHistory: true, + }, + }); + + return conversation; +}; + +export async function getContinuationAgentConversationHistory( + conversationId: string, +): Promise { + return await prisma.conversationHistory.findFirst({ + where: { + conversationId, + userType: "Agent", + deleted: null, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }); +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 787a769..bfa8e89 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -11,6 +11,8 @@ "typecheck": "tsc" }, "dependencies": { + "@ai-sdk/anthropic": "^1.2.12", + "@ai-sdk/google": "^1.2.22", "@ai-sdk/openai": "^1.3.21", "@coji/remix-auth-google": "^4.2.0", "@conform-to/react": "^0.6.1", @@ -52,6 +54,7 @@ "@tanstack/react-table": "^8.13.2", "@trigger.dev/sdk": "^3.3.17", "ai": "4.3.14", + "axios": "^1.10.0", "bullmq": "^5.53.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -61,11 +64,13 @@ "date-fns": "^4.1.0", "dayjs": "^1.11.10", "emails": "workspace:*", + "execa": "^9.6.0", "express": "^4.18.1", "graphology": "^0.26.0", "graphology-layout-force": "^0.2.4", "graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-noverlap": "^0.4.2", + "handlebars": "^4.7.8", "ioredis": "^5.6.1", "isbot": "^4.1.0", "jose": "^5.2.3", @@ -85,6 +90,7 @@ "remix-themes": "^1.3.1", "remix-typedjson": "0.3.1", "remix-utils": "^7.7.0", + "sdk": "link:@modelcontextprotocol/sdk", "sigma": "^3.0.2", "tailwind-merge": "^2.6.0", "tailwind-scrollbar-hide": "^2.0.0", diff --git a/packages/database/prisma/migrations/20250708000247_add_conversation/migration.sql b/packages/database/prisma/migrations/20250708000247_add_conversation/migration.sql new file mode 100644 index 0000000..4caae51 --- /dev/null +++ b/packages/database/prisma/migrations/20250708000247_add_conversation/migration.sql @@ -0,0 +1,70 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('Agent', 'User', 'System'); + +-- CreateTable +CREATE TABLE "Conversation" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deleted" TIMESTAMP(3), + "unread" BOOLEAN NOT NULL DEFAULT false, + "title" TEXT, + "userId" TEXT NOT NULL, + "workspaceId" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + + CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ConversationHistory" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deleted" TIMESTAMP(3), + "message" TEXT NOT NULL, + "userType" "UserType" NOT NULL, + "activityId" TEXT, + "context" JSONB, + "thoughts" JSONB, + "userId" TEXT, + "conversationId" TEXT NOT NULL, + + CONSTRAINT "ConversationHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ConversationExecutionStep" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deleted" TIMESTAMP(3), + "thought" TEXT NOT NULL, + "message" TEXT NOT NULL, + "actionId" TEXT, + "actionOutput" TEXT, + "actionInput" TEXT, + "actionStatus" TEXT, + "metadata" JSONB DEFAULT '{}', + "conversationHistoryId" TEXT NOT NULL, + + CONSTRAINT "ConversationExecutionStep_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConversationHistory" ADD CONSTRAINT "ConversationHistory_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConversationHistory" ADD CONSTRAINT "ConversationHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConversationHistory" ADD CONSTRAINT "ConversationHistory_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConversationExecutionStep" ADD CONSTRAINT "ConversationExecutionStep_conversationHistoryId_fkey" FOREIGN KEY ("conversationHistoryId") REFERENCES "ConversationHistory"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 3f8ea3a..a531549 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -10,67 +10,29 @@ generator client { previewFeatures = ["tracing"] } -model User { - id String @id @default(cuid()) - email String @unique - - authenticationMethod AuthenticationMethod - authenticationProfile Json? - authenticationExtraParams Json? - authIdentifier String? @unique - - displayName String? - name String? - avatarUrl String? - - memoryFilter String? // Adding memory filter instructions - - admin Boolean @default(false) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - marketingEmails Boolean @default(true) - confirmedBasicDetails Boolean @default(false) - - referralSource String? - - personalAccessTokens PersonalAccessToken[] - InvitationCode InvitationCode? @relation(fields: [invitationCodeId], references: [id]) - invitationCodeId String? - Space Space[] - Workspace Workspace? - IntegrationAccount IntegrationAccount[] - WebhookConfiguration WebhookConfiguration[] -} - -model Workspace { +model Activity { id String @id @default(uuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deleted DateTime? - name String - slug String @unique - icon String? + text String + // Used to link the task or activity to external apps + sourceURL String? - integrations String[] + integrationAccount IntegrationAccount? @relation(fields: [integrationAccountId], references: [id]) + integrationAccountId String? - userId String? @unique - user User? @relation(fields: [userId], references: [id]) - IngestionQueue IngestionQueue[] - IntegrationAccount IntegrationAccount[] - IntegrationDefinitionV2 IntegrationDefinitionV2[] - Activity Activity[] - WebhookConfiguration WebhookConfiguration[] + rejectionReason String? + + workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspaceId String + + WebhookDeliveryLog WebhookDeliveryLog[] + + ConversationHistory ConversationHistory[] } -enum AuthenticationMethod { - GOOGLE - MAGIC_LINK -} - -/// Used to generate PersonalAccessTokens, they're one-time use model AuthorizationCode { id String @id @default(cuid()) @@ -83,63 +45,69 @@ model AuthorizationCode { updatedAt DateTime @updatedAt } -// Used by User's to perform API actions -model PersonalAccessToken { - id String @id @default(cuid()) +model Conversation { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deleted DateTime? - /// If generated by the CLI this will be "cli", otherwise user-provided - name String + unread Boolean @default(false) - /// This is the token encrypted using the ENCRYPTION_KEY - encryptedToken Json - - /// This is shown in the UI, with ******** - obfuscatedToken String - - /// This is used to find the token in the database - hashedToken String @unique - - user User @relation(fields: [userId], references: [id]) + title String? + user User @relation(fields: [userId], references: [id]) userId String - revokedAt DateTime? - lastAccessedAt DateTime? + workspace Workspace? @relation(fields: [workspaceId], references: [id]) + workspaceId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + status String @default("pending") // Can be "pending", "running", "completed", "failed", "need_attension" - authorizationCodes AuthorizationCode[] + ConversationHistory ConversationHistory[] } -model InvitationCode { - id String @id @default(cuid()) - code String @unique +model ConversationExecutionStep { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deleted DateTime? - users User[] + thought String + message String - createdAt DateTime @default(now()) + actionId String? + actionOutput String? + actionInput String? + actionStatus String? + + metadata Json? @default("{}") + + conversationHistory ConversationHistory @relation(fields: [conversationHistoryId], references: [id]) + conversationHistoryId String } -// Space model for user workspaces -model Space { - id String @id @default(cuid()) - name String - description String? - autoMode Boolean @default(false) +model ConversationHistory { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deleted DateTime? - // Relations - user User @relation(fields: [userId], references: [id]) - userId String + message String + userType UserType - // Space's enabled entities - enabledEntities SpaceEntity[] + activity Activity? @relation(fields: [activityId], references: [id]) + activityId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - IngestionQueue IngestionQueue[] + context Json? + + thoughts Json? + user User? @relation(fields: [userId], references: [id]) + userId String? + + conversation Conversation @relation(fields: [conversationId], references: [id]) + conversationId String + ConversationExecutionStep ConversationExecutionStep[] } -// Entity types that can be stored in the memory plane model Entity { id String @id @default(cuid()) name String @unique // e.g., "User", "Issue", "Task", "Automation" @@ -152,27 +120,6 @@ model Entity { updatedAt DateTime @updatedAt } -// Junction table for Space-Entity relationship (what entities are enabled in each space) -model SpaceEntity { - id String @id @default(cuid()) - - // Relations - space Space @relation(fields: [spaceId], references: [id]) - spaceId String - - entity Entity @relation(fields: [entityId], references: [id]) - entityId String - - // Custom settings for this entity in this space - settings Json? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([spaceId, entityId]) -} - -// Queue for processing ingestion tasks model IngestionQueue { id String @id @default(cuid()) @@ -199,29 +146,6 @@ model IngestionQueue { processedAt DateTime? } -// For Integrations - -model Activity { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deleted DateTime? - - text String - // Used to link the task or activity to external apps - sourceURL String? - - integrationAccount IntegrationAccount? @relation(fields: [integrationAccountId], references: [id]) - integrationAccountId String? - - rejectionReason String? - - workspace Workspace @relation(fields: [workspaceId], references: [id]) - workspaceId String - - WebhookDeliveryLog WebhookDeliveryLog[] -} - model IntegrationAccount { id String @id @default(uuid()) createdAt DateTime @default(now()) @@ -265,6 +189,115 @@ model IntegrationDefinitionV2 { IntegrationAccount IntegrationAccount[] } +model InvitationCode { + id String @id @default(cuid()) + code String @unique + + users User[] + + createdAt DateTime @default(now()) +} + +model PersonalAccessToken { + id String @id @default(cuid()) + + /// If generated by the CLI this will be "cli", otherwise user-provided + name String + + /// This is the token encrypted using the ENCRYPTION_KEY + encryptedToken Json + + /// This is shown in the UI, with ******** + obfuscatedToken String + + /// This is used to find the token in the database + hashedToken String @unique + + user User @relation(fields: [userId], references: [id]) + userId String + + revokedAt DateTime? + lastAccessedAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + authorizationCodes AuthorizationCode[] +} + +model Space { + id String @id @default(cuid()) + name String + description String? + autoMode Boolean @default(false) + + // Relations + user User @relation(fields: [userId], references: [id]) + userId String + + // Space's enabled entities + enabledEntities SpaceEntity[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + IngestionQueue IngestionQueue[] +} + +model SpaceEntity { + id String @id @default(cuid()) + + // Relations + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + + entity Entity @relation(fields: [entityId], references: [id]) + entityId String + + // Custom settings for this entity in this space + settings Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([spaceId, entityId]) +} + +model User { + id String @id @default(cuid()) + email String @unique + + authenticationMethod AuthenticationMethod + authenticationProfile Json? + authenticationExtraParams Json? + authIdentifier String? @unique + + displayName String? + name String? + avatarUrl String? + + memoryFilter String? // Adding memory filter instructions + + admin Boolean @default(false) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + marketingEmails Boolean @default(true) + confirmedBasicDetails Boolean @default(false) + + referralSource String? + + personalAccessTokens PersonalAccessToken[] + InvitationCode InvitationCode? @relation(fields: [invitationCodeId], references: [id]) + invitationCodeId String? + Space Space[] + Workspace Workspace? + IntegrationAccount IntegrationAccount[] + WebhookConfiguration WebhookConfiguration[] + Conversation Conversation[] + ConversationHistory ConversationHistory[] +} + model WebhookConfiguration { id String @id @default(cuid()) url String @@ -298,9 +331,31 @@ model WebhookDeliveryLog { createdAt DateTime @default(now()) } -enum WebhookDeliveryStatus { - SUCCESS - FAILED +model Workspace { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deleted DateTime? + + name String + slug String @unique + icon String? + + integrations String[] + + userId String? @unique + user User? @relation(fields: [userId], references: [id]) + IngestionQueue IngestionQueue[] + IntegrationAccount IntegrationAccount[] + IntegrationDefinitionV2 IntegrationDefinitionV2[] + Activity Activity[] + WebhookConfiguration WebhookConfiguration[] + Conversation Conversation[] +} + +enum AuthenticationMethod { + GOOGLE + MAGIC_LINK } enum IngestionStatus { @@ -310,3 +365,14 @@ enum IngestionStatus { FAILED CANCELLED } + +enum UserType { + Agent + User + System +} + +enum WebhookDeliveryStatus { + SUCCESS + FAILED +} diff --git a/packages/types/src/conversation-execution-step/conversation-execution.entity.ts b/packages/types/src/conversation-execution-step/conversation-execution.entity.ts new file mode 100644 index 0000000..43bdeb5 --- /dev/null +++ b/packages/types/src/conversation-execution-step/conversation-execution.entity.ts @@ -0,0 +1,19 @@ +export enum ActionStatusEnum { + ACCEPT = "ACCEPT", + DECLINE = "DECLINE", + QUESTION = "QUESTION", + TOOL_REQUEST = "TOOL_REQUEST", + SUCCESS = "SUCCESS", + FAILED = "FAILED", +} + +export const ActionStatus = { + ACCEPT: "ACCEPT", + DECLINE: "DECLINE", + QUESTION: "QUESTION", + TOOL_REQUEST: "TOOL_REQUEST", + SUCCESS: "SUCCESS", + FAILED: "FAILED", +}; + +export type ActionStatus = (typeof ActionStatus)[keyof typeof ActionStatus]; diff --git a/packages/types/src/conversation-execution-step/index.ts b/packages/types/src/conversation-execution-step/index.ts new file mode 100644 index 0000000..cb952ac --- /dev/null +++ b/packages/types/src/conversation-execution-step/index.ts @@ -0,0 +1 @@ +export * from "./conversation-execution.entity"; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 555ffe9..a9bb73a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,2 +1,3 @@ export * from "./llm"; export * from "./graph"; +export * from "./conversation-execution-step"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47a523e..7a7ebf0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,12 @@ importers: apps/webapp: dependencies: + '@ai-sdk/anthropic': + specifier: ^1.2.12 + version: 1.2.12(zod@3.23.8) + '@ai-sdk/google': + specifier: ^1.2.22 + version: 1.2.22(zod@3.23.8) '@ai-sdk/openai': specifier: ^1.3.21 version: 1.3.22(zod@3.23.8) @@ -153,6 +159,9 @@ importers: ai: specifier: 4.3.14 version: 4.3.14(react@18.3.1)(zod@3.23.8) + axios: + specifier: ^1.10.0 + version: 1.10.0 bullmq: specifier: ^5.53.2 version: 5.53.2 @@ -180,6 +189,9 @@ importers: emails: specifier: workspace:* version: link:../../packages/emails + execa: + specifier: ^9.6.0 + version: 9.6.0 express: specifier: ^4.18.1 version: 4.21.2 @@ -195,6 +207,9 @@ importers: graphology-layout-noverlap: specifier: ^0.4.2 version: 0.4.2(graphology-types@0.24.8) + handlebars: + specifier: ^4.7.8 + version: 4.7.8 ioredis: specifier: ^5.6.1 version: 5.6.1 @@ -252,6 +267,9 @@ importers: remix-utils: specifier: ^7.7.0 version: 7.7.0(@remix-run/node@2.1.0(typescript@5.8.3))(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/router@1.23.0)(crypto-js@4.2.0)(react@18.3.1)(zod@3.23.8) + sdk: + specifier: link:@modelcontextprotocol/sdk + version: link:@modelcontextprotocol/sdk sigma: specifier: ^3.0.2 version: 3.0.2(graphology-types@0.24.8) @@ -463,6 +481,18 @@ importers: packages: + '@ai-sdk/anthropic@1.2.12': + resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/google@1.2.22': + resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/openai@1.3.22': resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==} engines: {node: '>=18'} @@ -3136,9 +3166,16 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@smithy/abort-controller@4.0.4': resolution: {integrity: sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==} engines: {node: '>=18.0.0'} @@ -4239,6 +4276,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.14: resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} engines: {node: ^10 || ^12 || >=14} @@ -4265,6 +4305,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -4516,6 +4559,10 @@ packages: color@3.2.1: resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -4925,6 +4972,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -5423,6 +5474,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -5484,6 +5539,10 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -5514,6 +5573,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -5522,6 +5590,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -5624,6 +5696,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -5724,6 +5800,11 @@ packages: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -5798,6 +5879,10 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + humanize-duration@3.33.0: resolution: {integrity: sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ==} @@ -6021,6 +6106,10 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -6041,6 +6130,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -6850,6 +6943,10 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + num2fraction@1.2.2: resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} @@ -7008,6 +7105,10 @@ packages: resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} engines: {node: '>=6'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -7363,6 +7464,10 @@ packages: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + prism-react-renderer@2.1.0: resolution: {integrity: sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==} peerDependencies: @@ -7418,6 +7523,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} @@ -8102,6 +8210,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -8471,6 +8583,11 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + ulid@2.4.0: resolution: {integrity: sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==} hasBin: true @@ -8489,6 +8606,10 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} @@ -8784,6 +8905,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -8876,6 +9000,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + zod-error@1.5.0: resolution: {integrity: sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==} @@ -8898,6 +9026,18 @@ packages: snapshots: + '@ai-sdk/anthropic@1.2.12(zod@3.23.8)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.23.8) + zod: 3.23.8 + + '@ai-sdk/google@1.2.22(zod@3.23.8)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.23.8) + zod: 3.23.8 + '@ai-sdk/openai@1.3.22(zod@3.23.8)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -10588,7 +10728,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10642,7 +10782,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10670,7 +10810,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10760,7 +10900,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10811,7 +10951,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10898,7 +11038,7 @@ snapshots: react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10939,7 +11079,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10967,7 +11107,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10987,7 +11127,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11006,7 +11146,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11032,7 +11172,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11192,7 +11332,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-toggle@1.1.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11203,7 +11343,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-tooltip@1.1.1(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11223,7 +11363,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11353,7 +11493,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -11843,11 +11983,15 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} + '@sec-ant/readable-stream@0.4.1': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 selderee: 0.11.0 + '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/abort-controller@4.0.4': dependencies: '@smithy/types': 4.3.1 @@ -12604,6 +12748,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@18.3.7(@types/react@18.2.47)': + dependencies: + '@types/react': 18.2.47 + '@types/react-dom@18.3.7(@types/react@18.2.69)': dependencies: '@types/react': 18.2.69 @@ -13221,6 +13369,8 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + autoprefixer@10.4.14(postcss@8.4.38): dependencies: browserslist: 4.25.0 @@ -13257,6 +13407,14 @@ snapshots: axe-core@4.10.3: {} + axios@1.10.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.3 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} bail@2.0.2: {} @@ -13533,6 +13691,10 @@ snapshots: color-convert: 1.9.3 color-string: 1.9.1 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@10.0.1: {} @@ -13940,6 +14102,8 @@ snapshots: dependencies: robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -14730,6 +14894,21 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + exit-hook@2.2.1: {} express@4.21.2: @@ -14816,6 +14995,10 @@ snapshots: fflate@0.4.8: {} + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -14859,6 +15042,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -14868,6 +15053,14 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + format@0.2.2: {} forwarded@0.2.0: {} @@ -14964,6 +15157,11 @@ snapshots: get-stream@8.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -15084,6 +15282,15 @@ snapshots: pumpify: 1.5.1 through2: 2.0.5 + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + hard-rejection@2.1.0: {} has-bigints@1.1.0: {} @@ -15169,6 +15376,8 @@ snapshots: human-signals@5.0.0: {} + human-signals@8.0.1: {} + humanize-duration@3.33.0: {} iconv-lite@0.4.24: @@ -15377,6 +15586,8 @@ snapshots: is-stream@3.0.0: {} + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -15398,6 +15609,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -16339,6 +16552,11 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + num2fraction@1.2.2: {} object-assign@4.1.1: {} @@ -16515,6 +16733,8 @@ snapshots: parse-ms@2.1.0: {} + parse-ms@4.0.0: {} + parseley@0.12.1: dependencies: leac: 0.6.0 @@ -16805,6 +17025,10 @@ snapshots: dependencies: parse-ms: 2.1.0 + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + prism-react-renderer@2.1.0(react@18.3.1): dependencies: '@types/prismjs': 1.26.5 @@ -16864,6 +17088,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + pseudomap@1.0.2: {} pump@2.0.1: @@ -16938,7 +17164,7 @@ snapshots: '@radix-ui/react-tooltip': 1.1.1(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/core': 1.3.101(@swc/helpers@0.5.2) '@types/react': 18.2.47 - '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@types/react-dom': 18.3.7(@types/react@18.2.47) '@types/webpack': 5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11) autoprefixer: 10.4.14(postcss@8.4.38) chalk: 4.1.2 @@ -17746,6 +17972,8 @@ snapshots: strip-final-newline@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -18147,6 +18375,9 @@ snapshots: ufo@1.6.1: {} + uglify-js@3.19.3: + optional: true + ulid@2.4.0: {} unbox-primitive@1.1.0: @@ -18162,6 +18393,8 @@ snapshots: undici@6.21.3: {} + unicorn-magic@0.3.0: {} + unified@10.1.2: dependencies: '@types/unist': 2.0.11 @@ -18564,6 +18797,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -18639,6 +18874,8 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors@2.1.1: {} + zod-error@1.5.0: dependencies: zod: 3.23.8 diff --git a/trigger/docker-compose.yaml b/trigger/docker-compose.yaml index 9419488..c0aac8c 100644 --- a/trigger/docker-compose.yaml +++ b/trigger/docker-compose.yaml @@ -24,7 +24,13 @@ services: # Only needed for bootstrap command: sh -c "chown -R node:node /home/node/shared && exec ./scripts/entrypoint.sh" healthcheck: - test: ["CMD", "node", "-e", "http.get('http://localhost:3000/healthcheck', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + test: + [ + "CMD", + "node", + "-e", + "http.get('http://localhost:3000/healthcheck', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))", + ] interval: 30s timeout: 10s retries: 5 @@ -76,8 +82,6 @@ services: image: electricsql/electric:${ELECTRIC_IMAGE_TAG:-1.0.13} restart: ${RESTART_POLICY:-unless-stopped} logging: *logging-config - depends_on: - - postgres networks: - webapp environment: @@ -107,7 +111,21 @@ services: networks: - webapp healthcheck: - test: ["CMD", "clickhouse-client", "--host", "localhost", "--port", "9000", "--user", "default", "--password", "password", "--query", "SELECT 1"] + test: + [ + "CMD", + "clickhouse-client", + "--host", + "localhost", + "--port", + "9000", + "--user", + "default", + "--password", + "password", + "--query", + "SELECT 1", + ] interval: 5s timeout: 5s retries: 5 @@ -233,7 +251,6 @@ services: volumes: shared: clickhouse: - shared: minio: networks: