From 22fde0fd5b8be4b04c85e9481e3144320b1c6f90 Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Thu, 11 Dec 2025 09:25:16 +0800 Subject: [PATCH 1/2] docs: remove outdated documentation files Remove 33 documentation files that are no longer needed: - Admin setup and deployment guides - Workflow-related documentation (architecture, patterns, types) - Integration guides (OneDrive, email providers, Shopify/Circuly) - API documentation - Internal design documents These docs have become stale and are being consolidated or deprecated. --- docs/admin_setup.md | 164 ---- docs/ai-workflow-assistant.md | 606 -------------- docs/api/create-conversation-message.md | 416 --------- docs/convex-local-setup.md | 149 ---- docs/convex-self-hosted-setup.md | 203 ----- docs/deployment-modes.md | 164 ---- docs/email-api-sending.md | 289 ------- docs/email-providers.md | 659 --------------- docs/integration-health-checks.md | 199 ----- docs/integrations-schema.md | 315 ------- docs/management-dashboard-user-guide.md | 289 ------- docs/onedrive-background-sync.md | 302 ------- docs/onedrive-debugging-guide.md | 203 ----- docs/onedrive-integration-guide.md | 565 ------------- docs/onedrive-parent-directory-tracking.md | 106 --- docs/row-level-security-guide.md | 651 --------------- docs/tale-db-deployment.md | 574 ------------- docs/tale-rag-deployment.md | 348 -------- docs/tone-of-voice-implementation.md | 278 ------ docs/url-configuration.md | 231 ----- docs/workflows/REFACTORING_SUMMARY.md | 232 ----- docs/workflows/architecture.md | 755 ----------------- docs/workflows/auto-processing-pattern.md | 355 -------- docs/workflows/data-spec.md | 516 ------------ docs/workflows/database-operations.md | 179 ---- docs/workflows/developer-guide.md | 680 --------------- docs/workflows/entity-finder-prompt-design.md | 279 ------- .../find-unprocessed-entities-tool.md | 331 -------- docs/workflows/manual-configuration.md | 224 ----- docs/workflows/version-management.md | 44 - docs/workflows/workflow-patterns-guide.md | 790 ------------------ docs/workflows/workflow-types.md | 385 --------- .../platform/convex/lib/variables/DESIGN.md | 528 ------------ 33 files changed, 12009 deletions(-) delete mode 100644 docs/admin_setup.md delete mode 100644 docs/ai-workflow-assistant.md delete mode 100644 docs/api/create-conversation-message.md delete mode 100644 docs/convex-local-setup.md delete mode 100644 docs/convex-self-hosted-setup.md delete mode 100644 docs/deployment-modes.md delete mode 100644 docs/email-api-sending.md delete mode 100644 docs/email-providers.md delete mode 100644 docs/integration-health-checks.md delete mode 100644 docs/integrations-schema.md delete mode 100644 docs/management-dashboard-user-guide.md delete mode 100644 docs/onedrive-background-sync.md delete mode 100644 docs/onedrive-debugging-guide.md delete mode 100644 docs/onedrive-integration-guide.md delete mode 100644 docs/onedrive-parent-directory-tracking.md delete mode 100644 docs/row-level-security-guide.md delete mode 100644 docs/tale-db-deployment.md delete mode 100644 docs/tale-rag-deployment.md delete mode 100644 docs/tone-of-voice-implementation.md delete mode 100644 docs/url-configuration.md delete mode 100644 docs/workflows/REFACTORING_SUMMARY.md delete mode 100644 docs/workflows/architecture.md delete mode 100644 docs/workflows/auto-processing-pattern.md delete mode 100644 docs/workflows/data-spec.md delete mode 100644 docs/workflows/database-operations.md delete mode 100644 docs/workflows/developer-guide.md delete mode 100644 docs/workflows/entity-finder-prompt-design.md delete mode 100644 docs/workflows/find-unprocessed-entities-tool.md delete mode 100644 docs/workflows/manual-configuration.md delete mode 100644 docs/workflows/version-management.md delete mode 100644 docs/workflows/workflow-patterns-guide.md delete mode 100644 docs/workflows/workflow-types.md delete mode 100644 services/platform/convex/lib/variables/DESIGN.md diff --git a/docs/admin_setup.md b/docs/admin_setup.md deleted file mode 100644 index bb2de9630c..0000000000 --- a/docs/admin_setup.md +++ /dev/null @@ -1,164 +0,0 @@ -# Admin User Setup Guide - -This guide explains how to set up the initial admin user for the Lanserhof application. - -## 🚀 Quick Start (Development) - -For local development, the system automatically creates a default admin user: - -- **Email**: `admin@lanserhof.local` -- **Password**: `admin123` -- **Role**: `Admin` - -⚠️ **IMPORTANT**: Change this password after first login! - -## 🔐 Production Setup - -For production environments, you should create a secure admin user: - -### Step 1: Generate Secure Password - -```bash -# Generate a random secure password and hash -pnpm admin:generate-password - -# Or use your own password -pnpm admin:generate-password "YourSecurePassword123!" -``` - -This will output: - -- The password (store this securely!) -- The bcrypt hash -- Environment variables to set - -### Step 2: Set Environment Variables - -Add these to your production environment: - -```bash -ADMIN_EMAIL="admin@yourdomain.com" -ADMIN_NAME="System Administrator" -ADMIN_PASSWORD_HASH="$2b$12$..." -``` - -### Step 3: Deploy and Reset Database - -When you deploy with these environment variables, the seed script will create the admin user with your secure credentials. - -## 🛠️ Manual Admin User Creation - -If you need to manually create an admin user, you can use SQL: - -```sql --- Generate a password hash first using the script --- Then insert the user -INSERT INTO next_auth.users ( - id, - name, - email, - "emailVerified", - password_hash, - role, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - 'Your Name', - 'your-email@domain.com', - NOW(), - 'your-bcrypt-hash-here', - 'Admin', - NOW(), - NOW() -); -``` - -## 🔄 Changing Admin Password - -### Method 1: Through the Application (Recommended) - -1. Log in with current admin credentials -2. Go to Profile/Settings -3. Change password through the UI - -### Method 2: Database Update - -```sql --- Generate new hash using the script first -UPDATE next_auth.users -SET password_hash = 'new-bcrypt-hash-here', - updated_at = NOW() -WHERE email = 'admin@yourdomain.com' AND role = 'Admin'; -``` - -## 🔍 Verifying Admin User - -To check if the admin user exists: - -```sql -SELECT id, name, email, role, created_at -FROM next_auth.users -WHERE role = 'Admin'; -``` - -## 🏗️ Role Hierarchy - -The system uses a hierarchical role system: - -- **Member** (Level 1) - Basic access -- **Editor** (Level 2) - Can edit content + Member permissions -- **Developer** (Level 3) - Can access dev tools + Editor permissions -- **Admin** (Level 4) - Full system access + Developer permissions - -## 🔒 Security Best Practices - -1. **Use Strong Passwords**: Minimum 12 characters with mixed case, numbers, and symbols -2. **Change Default Passwords**: Always change the default `admin123` password -3. **Limit Admin Users**: Only create admin users when necessary -4. **Regular Password Updates**: Update admin passwords regularly -5. **Monitor Admin Activity**: Keep track of admin user actions - -## 🚨 Troubleshooting - -### Admin User Not Created - -If the admin user isn't created during database initialization: - -1. Check the seed.sql file is present -2. Verify environment variables are set correctly -3. Check database logs for errors -4. Manually run the seed script - -### Can't Log In - -1. Verify the email and password are correct -2. Check if the user exists in the database -3. Ensure the password hash is valid -4. Check Auth.js configuration - -### Password Hash Issues - -If you're having issues with password hashes: - -```bash -# Test password hashing -node -e " -const bcrypt = require('bcryptjs'); -bcrypt.hash('your-password', 12).then(hash => { - console.log('Hash:', hash); - bcrypt.compare('your-password', hash).then(valid => { - console.log('Valid:', valid); - }); -}); -" -``` - -## 📞 Support - -If you encounter issues with admin user setup, check: - -1. Database migration logs -2. Supabase dashboard for user data -3. Application logs for authentication errors -4. Environment variable configuration diff --git a/docs/ai-workflow-assistant.md b/docs/ai-workflow-assistant.md deleted file mode 100644 index ee396a2f1e..0000000000 --- a/docs/ai-workflow-assistant.md +++ /dev/null @@ -1,606 +0,0 @@ -# AI Workflow Assistant - Complete Guide - -## Overview - -The AI-powered workflow assistant enables users to create, modify, and understand workflows through natural language conversations. It combines AI intelligence with workflow automation to make complex automation accessible to all users. - -## Architecture - -### Components - -1. **Workflow Tools** (`convex/agent_tools/convex_tools/workflows/`) - - - `get_workflow_structure.ts` - Retrieves complete workflow structure - - `update_workflow_step.ts` - Updates existing steps (for small, targeted edits) - - `generate_workflow_from_description.ts` - AI-powered initial workflow generation - - `save_workflow_definition.ts` - Saves/updates an entire workflow (metadata + all steps) in one atomic operation - - `list_available_actions.ts` - Discovers all available action types - - `search_workflow_examples.ts` - Searches existing workflows for examples - -2. **Workflow Assistant Agent** (`convex/workflow_assistant_agent.ts`) - - - Main entry point for AI conversations - - Automatically loads workflow context - - Handles tool execution and response generation - -3. **Agent Configuration** (`convex/lib/create_workflow_agent.ts`) - - - Specialized agent with workflow-specific instructions - - Lower temperature (0.3) for consistent generation - - Comprehensive system prompt with examples - -4. **Frontend Integration** (`app/.../automation-sidepanel.tsx`) - - Real-time chat interface - - Connects to workflow assistant backend - - Context-aware conversations - -## Features - -### 1. Workflow Understanding - -The AI can analyze and explain existing workflows: - -- Reads workflow structure and steps -- Explains what each step does -- Identifies potential improvements - -### 2. Workflow Creation - -Generate complete workflows from natural language: - -``` -User: "Create a workflow that sends product recommendations to inactive customers" -AI: [Generates complete workflow with trigger, data fetching, AI analysis, and email steps] -``` - -### 3. Workflow Modification - -Make targeted changes to existing workflows: - -- Add new steps -- Update step configurations -- Remove unnecessary steps -- Modify step connections - -### 4. Context Awareness - -The assistant automatically: - -- Loads current workflow structure when workflowId is provided -- Maintains conversation context across messages -- Provides relevant suggestions based on workflow state - -### 5. Knowledge Discovery - -Two powerful knowledge tools help the AI discover and learn: - -#### `list_available_actions` 🔧 - -Discovers all available action types that can be used in workflow steps. - -**Returns:** - -- Action type (e.g., `customer`, `product`, `conversation`, `email_provider`) -- Title and description -- All operations supported (e.g., `create`, `query`, `update`) -- All parameters (required and optional) -- Category (customer, product, email, conversation, etc.) -- Usage example with proper syntax - -**Categories available:** - -- `customer` - Customer management operations -- `product` - Product management operations -- `email` - Email sending operations -- `conversation` - Conversation/messaging operations -- `document` - Document management operations -- `integration` - Third-party integrations (Shopify, Circuly, OneDrive) -- `workflow` - Workflow-specific operations (approvals, processing records) -- `knowledge` - Knowledge base operations (RAG) -- `web` - Web crawling and website operations - -#### `search_workflow_examples` 📚 - -Searches existing workflows to find examples and learn from existing structures. - -**Returns:** - -- Workflow name and description -- Status (active/inactive) -- Step count -- Step structure (includes full config) - - Step ID, name, type, order - - Full step config - - Next steps connections - -**Search tips:** - -- Use keywords like "email", "customer", "product", "recommendation" -- Set `includeInactive: true` to see draft/inactive workflows -- Adjust `limit` to control number of results (default: 5) - -## Usage Examples - -### Creating a New Workflow - -**User:** "I want to create a workflow that finds customers who haven't purchased in 30 days and sends them personalized product recommendations" - -**AI Response:** - -1. Searches for similar examples using `search_workflow_examples` -2. Discovers available actions using `list_available_actions` -3. Analyzes the requirement and breaks it down into logical steps -4. Uses `generate_workflow_from_description` tool -5. Creates workflow with: - - Trigger (scheduled daily) - - Find inactive customers (action) - - Analyze purchase history (LLM) - - Generate recommendations (LLM) - - Send email (action) - -### Modifying an Existing Workflow - -**User:** "Add a step that checks if customers have opened previous emails" - -**AI Response:** - -1. Uses `get_workflow_structure` to understand current workflow -2. Identifies where to insert the new step -3. Regenerates the full list of steps (including the new email engagement check) -4. Uses `save_workflow_definition` to replace the workflow's steps atomically -5. Confirms the changes - -### Understanding a Workflow - -**User:** "What does this workflow do?" - -**AI Response:** - -1. Uses `get_workflow_structure` to load workflow -2. Analyzes each step -3. Provides clear explanation of: - - Overall purpose - - Each step's function - - How steps connect - - Potential improvements - -## Technical Details - -### Tool Registry Integration - -All workflow tools are registered in `convex/agent_tools/tool_registry.ts`: - -```typescript -export const TOOL_REGISTRY = [ - // ... existing tools - getWorkflowStructureTool, - updateWorkflowStepTool, - generateWorkflowFromDescriptionTool, - saveWorkflowDefinitionTool, - listAvailableActionsTool, - searchWorkflowExamplesTool, -] as const; -``` - -### Workflow Context Loading - -When a `workflowId` is provided, the assistant automatically loads: - -- Workflow name, description, and status -- All steps with their configurations -- Step connections and order -- This context is injected into the conversation - -### AI Model Configuration - -- **Model:** GPT-4o (configurable via `OPENAI_MODEL` env var) -- **Temperature:** 0.3 (lower for consistent workflow generation) -- **Max Steps:** 15 (allows complex multi-step operations) - -### Available Step Types - -1. **trigger** - Starts workflow (manual, scheduled, event-based) -2. **llm** - AI agent with tools and decision-making -3. **action** - Database queries, API calls, operations -4. **condition** - Branching logic based on expressions -5. **loop** - Iteration over collections - -### Workflow Creation Flow - -1. **User requests a workflow** - - ``` - User: "Create a workflow that sends abandoned cart emails" - ``` - -2. **AI searches for similar examples** - - ``` - AI: search_workflow_examples({ query: "abandoned cart email" }) - ``` - -3. **AI discovers available actions** - - ``` - AI: list_available_actions({ category: "email" }) - AI: list_available_actions({ category: "customer" }) - ``` - -4. **AI generates the workflow** - - Uses patterns from examples - - Uses correct action types and operations - - Includes all required parameters - - Follows best practices - -### Benefits - -✅ **Consistency** - AI learns from existing workflows and maintains consistent patterns -✅ **Accuracy** - AI knows exactly what actions and operations are available -✅ **Completeness** - AI includes all required parameters -✅ **Best Practices** - AI follows patterns that have been proven to work - -## Best Practices - -### For Users - -1. **Be Specific** - Provide clear descriptions of what you want -2. **Ask Questions** - The AI will clarify if needed -3. **Review Changes** - Always review AI-generated workflows -4. **Iterate** - Start simple and add complexity gradually - -### For Developers - -1. **Tool Design** - Keep tools focused and single-purpose -2. **Error Handling** - Always return success/failure status -3. **Context** - Pass organizationId and workflowId in context -4. **Validation** - Validate AI-generated configurations -5. **Testing** - Test with various natural language inputs - -## Testing Guide - -### Quick Start Testing - -#### 1. Test Workflow Creation - -Open the workflow editor and click the AI Assistant button. Try these prompts: - -**Simple Workflow:** - -``` -Create a workflow that sends a welcome email to new customers -``` - -**Expected Result:** - -- Workflow with trigger step -- Action to send email -- Proper step connections - -**Complex Workflow:** - -``` -Create a workflow that: -1. Finds customers who haven't purchased in 30 days -2. Analyzes their purchase history using AI -3. Generates 3-5 product recommendations -4. Sends a personalized email with recommendations -``` - -**Expected Result:** - -- Multi-step workflow with trigger, actions, and LLM steps -- Proper tool configuration for LLM steps -- Logical step ordering and connections - -#### 2. Test Workflow Modification - -Open an existing workflow and ask: - -``` -Add a step that checks if the customer has opened previous emails before sending -``` - -**Expected Result:** - -- AI loads current workflow structure -- Adds condition step in appropriate location -- Updates step connections -- Confirms the change - -#### 3. Test Workflow Understanding - -Open an existing workflow and ask: - -``` -What does this workflow do? -``` - -**Expected Result:** - -- Clear explanation of workflow purpose -- Description of each step -- How steps connect together -- Potential suggestions for improvement - -#### 4. Test Context Awareness - -In an existing workflow, ask: - -``` -How many steps does this workflow have? -``` - -**Expected Result:** - -- Accurate count of steps -- Brief description of step types - -Then ask: - -``` -Can you add a loop to process multiple customers? -``` - -**Expected Result:** - -- AI understands the current workflow context -- Suggests where to add the loop -- Creates the loop step with proper configuration - -### Test Scenarios - -#### Scenario 1: E-commerce Re-engagement - -**Prompt:** - -``` -Create a customer re-engagement workflow that: -- Runs daily at 9 AM -- Finds customers inactive for 30+ days -- Uses AI to analyze their browsing and purchase history -- Generates personalized product recommendations -- Sends email only if customer has good email engagement -``` - -**Validation:** - -- [ ] Trigger step with schedule configuration -- [ ] Action step to find inactive customers -- [ ] LLM step with appropriate tools (customer_search, list_products) -- [ ] Condition step for email engagement check -- [ ] Action step to send email -- [ ] All steps properly connected - -#### Scenario 2: Product Recommendation - -**Prompt:** - -``` -I need a workflow that recommends products based on what customers viewed but didn't buy -``` - -**Validation:** - -- [ ] Workflow created with logical steps -- [ ] Uses customer and product tools -- [ ] Includes AI analysis step -- [ ] Has email/notification step - -#### Scenario 3: Workflow Modification - -**Setup:** Open existing workflow with 3 steps - -**Prompt:** - -``` -Add a step between step 2 and step 3 that validates the data -``` - -**Validation:** - -- [ ] AI loads current workflow -- [ ] Creates new step with order 2.5 or reorders existing steps -- [ ] Updates connections properly -- [ ] Confirms the change - -#### Scenario 4: Error Handling - -**Prompt:** - -``` -Add error handling to this workflow -``` - -**Validation:** - -- [ ] AI suggests adding condition steps -- [ ] Adds failure paths in nextSteps -- [ ] Suggests error notification steps - -### Manual Testing Checklist - -#### Backend Tests - -- [ ] `get_workflow_structure` tool returns correct data -- [ ] `create_workflow_step` tool creates steps successfully -- [ ] `update_workflow_step` tool updates configurations -- [ ] `delete_workflow_step` tool removes steps -- [ ] `generate_workflow_from_description` creates valid workflows -- [ ] `list_available_actions` returns all action types -- [ ] `search_workflow_examples` finds relevant workflows -- [ ] Workflow context loads automatically when workflowId provided -- [ ] organizationId is properly passed to tools - -#### Frontend Tests - -- [ ] AI Assistant panel opens/closes correctly -- [ ] Welcome message appears when panel opens -- [ ] User messages display correctly -- [ ] AI responses stream/display properly -- [ ] Loading state shows during AI processing -- [ ] Error messages display when API fails -- [ ] Thread ID is unique per workflow -- [ ] Panel is resizable - -#### Integration Tests - -- [ ] Created workflows appear in workflow list -- [ ] Modified workflows update in real-time -- [ ] Deleted steps are removed from database -- [ ] Workflow execution works with AI-generated workflows -- [ ] Tool calls are logged and visible -- [ ] Context is maintained across multiple messages - -### Performance Testing - -#### Response Time - -- [ ] Simple queries respond in < 3 seconds -- [ ] Complex workflow generation completes in < 10 seconds -- [ ] Workflow structure loading is fast (< 1 second) - -#### Reliability - -- [ ] AI consistently generates valid workflow structures -- [ ] Tool calls succeed reliably -- [ ] Error handling works for invalid inputs -- [ ] Context loading handles missing workflows gracefully - -### Edge Cases - -#### Test Invalid Inputs - -1. **Empty workflow description** - - ``` - Create a workflow - ``` - - Expected: AI asks for more details - -2. **Ambiguous request** - - ``` - Make it better - ``` - - Expected: AI asks what to improve - -3. **Non-existent workflow** - - - Open AI chat without workflowId - - Ask about "this workflow" - Expected: AI explains no workflow is loaded - -4. **Invalid step configuration** - ``` - Add a step with type "invalid_type" - ``` - Expected: AI suggests valid step types - -### Debugging Tips - -#### Enable Verbose Logging - -Check browser console for: - -- Tool execution logs -- API call responses -- Error messages - -Check Convex logs for: - -- Agent execution traces -- Tool call results -- Workflow creation/modification logs - -#### Common Issues - -**Issue:** AI doesn't create workflow - -- Check: organizationId is provided -- Check: OPENAI_API_KEY is set -- Check: Tool registry includes workflow tools - -**Issue:** Context not loading - -- Check: workflowId is valid -- Check: Workflow exists in database -- Check: User has access to workflow - -**Issue:** Tools not executing - -- Check: Tool names match registry -- Check: Context includes organizationId -- Check: Tool handlers don't throw errors - -### Success Criteria - -The AI Workflow Assistant is working correctly when: - -✅ Users can create workflows from natural language descriptions -✅ AI generates valid, executable workflow structures -✅ Workflow modifications work reliably -✅ Context is maintained throughout conversations -✅ Error messages are clear and helpful -✅ Response times are acceptable (< 10s for complex operations) -✅ Tool calls succeed consistently -✅ Generated workflows execute successfully - -## Troubleshooting - -### Common Issues - -**Issue:** AI doesn't understand the request - -- **Solution:** Be more specific, provide examples - -**Issue:** Workflow generation fails - -- **Solution:** Check logs, verify organizationId is provided - -**Issue:** Tools not executing - -- **Solution:** Verify tool registry, check context variables - -**Issue:** Context not loading - -- **Solution:** Ensure workflowId is valid and workflow exists - -## API Reference - -### `chatWithWorkflowAssistant` - -```typescript -action({ - args: { - threadId: v.string(), - organizationId: v.string(), - workflowId: v.optional(v.id('wfDefinitions')), - message: v.string(), - maxSteps: v.optional(v.number()), - }, - returns: v.object({ - response: v.string(), - toolCalls: v.optional(v.array(...)), - }), -}) -``` - -## Future Enhancements - -Potential improvements to consider: - -1. **Undo/Redo** - Track AI actions for easy reversal -2. **Templates** - Pre-built workflow templates -3. **Visual Feedback** - Highlight changes in the workflow canvas -4. **Validation** - Pre-execution workflow validation -5. **Testing** - AI-assisted workflow testing -6. **Optimization** - AI suggestions for performance improvements -7. **Documentation** - Auto-generate workflow documentation - -## Conclusion - -The AI Workflow Assistant provides a powerful, intuitive way to create and manage workflows through natural language. It combines the flexibility of AI with the structure of workflow automation, making complex automation accessible to all users. - -The knowledge tools (`list_available_actions` and `search_workflow_examples`) ensure the AI generates consistent, accurate workflows by learning from existing patterns and understanding available capabilities. diff --git a/docs/api/create-conversation-message.md b/docs/api/create-conversation-message.md deleted file mode 100644 index bde7164f61..0000000000 --- a/docs/api/create-conversation-message.md +++ /dev/null @@ -1,416 +0,0 @@ -# Conversation API - Convex Functions - -## Overview - -This document describes the Convex functions available for managing conversations and messages. All functions can be called directly from client components using Convex React hooks or from server components using `fetchQuery`/`fetchMutation`. - -## Mutations - -### `createConversation` - -Creates a new parent conversation. - -**Parameters:** - -```typescript -{ - organizationId: Id<'organizations'>; - customerId?: Id<'customers'>; - subject?: string; - status?: string; // Default: 'open' - priority?: string; // Default: 'medium' - metadata?: any; -} -``` - -**Returns:** `Id<'conversations'>` - -**Usage (Client):** - -```typescript -import { useMutation } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const createConv = useMutation(api.conversations.createConversation); - -await createConv({ - organizationId: orgId, - customerId: custId, - subject: 'Customer inquiry', - status: 'open', - priority: 'high', -}); -``` - ---- - -### `addMessageToConversation` - -Adds a message as a child conversation to an existing conversation. - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; - organizationId: Id<'organizations'>; - sender: string; - content: string; - isCustomer: boolean; - status?: string; // Default: 'sent' - attachment?: any; -} -``` - -**Returns:** `Id<'conversations'>` (message ID) - -**Usage (Client):** - -```typescript -const addMessage = useMutation(api.conversations.addMessageToConversation); - -await addMessage({ - conversationId: convId, - organizationId: orgId, - sender: 'Agent Name', - content: 'Thank you for contacting us...', - isCustomer: false, - status: 'sent', -}); -``` - ---- - -### `closeConversation` - -Closes a conversation. - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; - resolvedBy?: string; -} -``` - -**Returns:** `null` - ---- - -### `reopenConversation` - -Reopens a resolved conversation. - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; -} -``` - -**Returns:** `null` - ---- - -### `markConversationAsSpam` - -Marks a conversation as spam. - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; -} -``` - -**Returns:** `null` - ---- - -### `markConversationAsRead` - -Marks a conversation as read and resets unread count. - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; -} -``` - -**Returns:** `null` - ---- - -### `bulkCloseConversations` - -Closes multiple conversations at once. - -**Parameters:** - -```typescript -{ - conversationIds: Id<'conversations'>[]; - resolvedBy?: string; -} -``` - -**Returns:** - -```typescript -{ - successCount: number; - failedCount: number; -} -``` - ---- - -### `bulkReopenConversations` - -Reopens multiple conversations at once. - -**Parameters:** - -```typescript -{ - conversationIds: Id < 'conversations' > []; -} -``` - -**Returns:** - -```typescript -{ - successCount: number; - failedCount: number; -} -``` - ---- - -## Queries - -### `getConversations` - -Gets conversations for an organization with filtering and pagination. - -**Parameters:** - -```typescript -{ - organizationId: Id<'organizations'>; - status?: string; - priority?: string; - search?: string; - page?: number; // Default: 1 - limit?: number; // Default: 20 -} -``` - -**Returns:** - -```typescript -{ - conversations: Conversation[]; // Array of conversations with messages - total: number; - page: number; - limit: number; - totalPages: number; -} -``` - -**Usage (Client):** - -```typescript -import { useQuery } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const conversations = useQuery(api.conversations.getConversations, { - organizationId: orgId, - status: 'open', - page: 1, - limit: 20, -}); -``` - -**Usage (Server):** - -```typescript -import { fetchQuery } from 'convex/nextjs'; -import { api } from '@/convex/_generated/api'; - -const conversations = await fetchQuery(api.conversations.getConversations, { - organizationId: orgId, - status: 'open', -}); -``` - ---- - -### `getConversation` - -Gets a single conversation by ID (without messages). - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; -} -``` - -**Returns:** `Conversation | null` - ---- - -### `getConversationWithMessages` - -Gets a conversation with all its messages and customer data. - -**Parameters:** - -```typescript -{ - conversationId: Id<'conversations'>; -} -``` - -**Returns:** `ConversationWithMessages | null` - -**Usage (Client):** - -```typescript -const conversation = useQuery(api.conversations.getConversationWithMessages, { - conversationId: convId, -}); -``` - ---- - -## Data Model - -### Conversation Structure - -Conversations use a hierarchical model where: - -- **Parent conversations** represent the main conversation thread -- **Child conversations** (where `parentId` is set) represent individual messages - -### Key Fields - -- `organizationId` - ID of the organization -- `customerId` - Optional customer ID -- `subject` - Conversation subject/title -- `status` - Status: 'pending', 'resolved', 'spam', 'archived' -- `priority` - Priority: 'low', 'medium', 'high' -- `parentId` - For child conversations (messages), references parent -- `metadata` - Flexible JSON field for additional data - -### Metadata Fields (Parent Conversations) - -```typescript -{ - description?: string; - channel?: string; // e.g., 'Email' - category?: string; // e.g., 'General', 'ProductRecommendation' - unread_count?: number; - last_message_at?: number; - last_read_at?: string; - resolved_at?: string; - resolved_by?: string; - marked_spam_at?: string; -} -``` - -### Metadata Fields (Child Conversations/Messages) - -```typescript -{ - sender: string; - content: string; - isCustomer: boolean; - attachment?: any; -} -``` - ---- - -## Best Practices - -1. **Use `useMutation` for writes**: In client components, use the `useMutation` hook for all mutations. - -2. **Use `useQuery` for reads**: In client components, use the `useQuery` hook for reactive queries. - -3. **Use `fetchQuery`/`fetchMutation` in Server Components**: For Next.js server components or actions. - -4. **Refresh after mutations**: Call `router.refresh()` after mutations in server-rendered pages to update the UI. - -5. **Handle errors**: Always wrap mutations in try-catch blocks and provide user feedback. - -6. **Batch operations**: Use bulk mutations when performing operations on multiple conversations. - -## Example: Complete Conversation Flow - -```typescript -'use client'; - -import { useMutation, useQuery } from 'convex/react'; -import { api } from '@/convex/_generated/api'; -import { useRouter } from 'next/navigation'; - -export function ConversationComponent({ orgId, customerId }) { - const router = useRouter(); - - // Queries - const conversations = useQuery(api.conversations.getConversations, { - organizationId: orgId, - status: 'open', - }); - - // Mutations - const createConv = useMutation(api.conversations.createConversation); - const addMessage = useMutation(api.conversations.addMessageToConversation); - const close = useMutation(api.conversations.closeConversation); - - const handleCreateAndReply = async () => { - try { - // Create conversation - const convId = await createConv({ - organizationId: orgId, - customerId: customerId, - subject: 'New inquiry', - }); - - // Add message - await addMessage({ - conversationId: convId, - organizationId: orgId, - sender: 'Agent', - content: 'Hello! How can we help?', - isCustomer: false, - }); - - // Close after reply - await close({ conversationId: convId }); - - router.refresh(); - } catch (error) { - console.error('Error:', error); - } - }; - - return ( -
- - {/* Render conversations */} -
- ); -} -``` diff --git a/docs/convex-local-setup.md b/docs/convex-local-setup.md deleted file mode 100644 index 82468a9f15..0000000000 --- a/docs/convex-local-setup.md +++ /dev/null @@ -1,149 +0,0 @@ -# Convex Local Development Setup - -This guide explains how to set up Convex for local development in this project. - -## 🔧 Important: Always Use Local Mode - -This project is configured to use **Convex in local development mode**. This means: - -- ✅ No cloud account required -- ✅ All data stored locally on your machine -- ✅ Faster development iteration -- ✅ Works completely offline -- ✅ No external dependencies or API keys needed for basic development - -## First-Time Setup - -**✅ No Setup Required!** - -This project is configured to automatically skip the Convex login prompt using `CONVEX_AGENT_MODE=anonymous`. You will **NOT** see any setup prompts when running the development server. - -The system automatically: - -- Skips authentication prompts -- Runs in anonymous local mode -- Creates a local-only development environment - -### Why Local Development? - -1. **Simplicity**: No need to create cloud accounts or manage API keys -2. **Speed**: Faster startup and iteration times -3. **Privacy**: All your data stays on your local machine -4. **Reliability**: No network dependencies for core development - -## Running the Development Server - -The recommended way to start development is: - -```bash -npm run dev -``` - -This script will: - -1. 🔧 Start Convex backend in local mode (port 3210) -2. ⏳ Wait for the backend to be ready -3. 🔄 Sync environment variables from `.env.local` -4. ✅ Generate Convex code and types -5. 🚀 Start Next.js dev server (port 3000) - -You'll see helpful messages like: - -``` -[dev] 🔧 Using LOCAL development mode for Convex -[dev] 💡 If this is your first time running Convex and you see a setup prompt: -[dev] Please choose "Local development" or "Local" option -[dev] This ensures you're running in local mode without cloud dependencies -``` - -## Manual Convex Commands - -If you need to run Convex commands manually, always use the `CONVEX_AGENT_MODE=anonymous` environment variable: - -```bash -# Start Convex backend only (no login prompt, automatically local) -CONVEX_AGENT_MODE=anonymous npx convex dev - -# Generate code and types -npx convex codegen - -# Deploy functions to local backend (no login prompt, automatically local) -CONVEX_AGENT_MODE=anonymous npx convex deploy -``` - -## Package.json Scripts - -The following npm scripts are available and pre-configured for anonymous local development: - -```bash -# Main development server (recommended) -npm run dev - -# Convex-only commands (all use CONVEX_AGENT_MODE=anonymous) -npm run convex:dev # Starts Convex in anonymous mode (automatically local) -npm run convex:deploy # Deploys to anonymous backend (automatically local) -npm run convex:codegen # Generates types and API -``` - -## Troubleshooting - -### "Convex backend did not start listening within 30s" - -This usually happens if: - -1. You chose "Cloud development" instead of "Local development" in the setup prompt -2. There's a port conflict on port 3210 - -**Solution**: - -- Kill any existing processes and restart -- Make sure to choose "Local development" if prompted again -- Check that port 3210 is available - -### "Setup prompt appears despite configuration" - -If you somehow still see a setup prompt (this should not happen with the current configuration): - -**Solution**: - -1. Stop all development servers -2. Run `CONVEX_AGENT_MODE=anonymous npx convex dev --local` manually -3. The prompt should be automatically skipped -4. If it still appears, check that the environment variable is properly set - -### Port 3210 already in use - -If you see an error about port 3210 being in use: - -```bash -# Find and kill the process using port 3210 -lsof -ti:3210 | xargs kill -9 - -# Or restart your development server -npm run dev -``` - -## Data Storage - -In local mode, Convex stores all data in a local directory. Your data persists between restarts, so you don't lose your development data when stopping and starting the server. - -## Environment Variables - -The development script automatically syncs environment variables from your `.env.local` file to the Convex backend. This includes: - -- `SITE_URL` -- Authentication keys -- API keys for external services - -Make sure your `.env.local` file is properly configured before starting development. - -## Next Steps - -Once you have Convex running locally: - -1. Your Next.js app will be available at `http://localhost:3000` -2. Convex dashboard (if needed) at `http://localhost:3210` -3. Make changes to Convex functions in the `convex/` directory -4. Changes will automatically reload and sync - -Happy coding! 🚀 diff --git a/docs/convex-self-hosted-setup.md b/docs/convex-self-hosted-setup.md deleted file mode 100644 index 3c1f01362d..0000000000 --- a/docs/convex-self-hosted-setup.md +++ /dev/null @@ -1,203 +0,0 @@ -# Convex Self-Hosted Setup Guide - -This guide explains how to configure and use Convex self-hosted backend with PostgreSQL in the Tale platform. - -## Overview - -The Tale platform uses Convex self-hosted backend for real-time data synchronization and serverless functions. The backend is configured to use PostgreSQL as the persistence layer. - -## Configuration - -### Environment Variables - -The following environment variables are required in your `.env` file: - -```bash -# PostgreSQL connection for Convex backend -# IMPORTANT: Do NOT include database name or query parameters -POSTGRES_URL=postgresql://tale:tale_password_change_me@db:5432 - -# Instance name (also used as database name) -INSTANCE_NAME=tale_platform - -# Optional: Disable SSL requirement for local development -DO_NOT_REQUIRE_SSL=1 -``` - -### Key Points - -1. **POSTGRES_URL Format**: - - - ✅ **Correct**: `postgresql://user:password@host:port` - - ❌ **Wrong**: `postgresql://user:password@host:port/database_name` - - Do NOT include database name or query parameters - -2. **Database Name**: - - - The database name is specified by `INSTANCE_NAME` - - Default: `tale_platform` - -3. **Database Creation**: - - The database is automatically created by the init script using the `INSTANCE_NAME` variable - - Default database name: `tale_platform` - - Location: `services/db/init-scripts/02-create-convex-database.sql` - - To use a different database name, set `INSTANCE_NAME` in your `.env` file - -## Docker Compose Configuration - -The `compose.yml` file is configured to pass the correct environment variables to the platform service: - -```yaml -environment: - # PostgreSQL connection (without database name) - POSTGRES_URL: ${POSTGRES_URL:-postgresql://tale:tale_password_change_me@db:5432} - - # Instance configuration - INSTANCE_NAME: ${INSTANCE_NAME:-tale_platform} - INSTANCE_SECRET: ${INSTANCE_SECRET} - - # Convex URLs - CONVEX_CLOUD_ORIGIN: ${CONVEX_CLOUD_ORIGIN:-http://127.0.0.1:3210} - CONVEX_SITE_ORIGIN: ${CONVEX_SITE_ORIGIN:-http://127.0.0.1:3211} - - # Security - DO_NOT_REQUIRE_SSL: ${DO_NOT_REQUIRE_SSL:-false} -``` - -## Verification - -### 1. Check Database Creation - -After starting the services, verify the database was created: - -```bash -# Connect to PostgreSQL -docker compose exec db psql -U tale -d tale_platform - -# List tables (should show Convex tables after first deployment) -\dt -``` - -### 2. Check Convex Backend Logs - -```bash -# View platform service logs -docker compose logs platform - -# Look for successful database connection -# Expected log: "Connected to Postgres" -``` - -### 3. Generate Admin Key - -```bash -# Generate an admin key for CLI access -docker compose exec platform ./generate_admin_key.sh -``` - -## Usage - -### Deploy Convex Functions - -```bash -# In your project directory -npx convex dev -``` - -### Environment Variables for Development - -Create a `.env.local` file in your project root: - -```bash -CONVEX_SELF_HOSTED_URL='http://localhost:3210' -CONVEX_SELF_HOSTED_ADMIN_KEY='' -``` - -## Troubleshooting - -### Issue: "Failed to connect to database" - -**Solution**: Verify that: - -1. `POSTGRES_URL` does NOT include the database name -2. The database `tale_platform` exists -3. The PostgreSQL service is running - -### Issue: "Database not found" - -**Solution**: - -1. Check that `INSTANCE_NAME` matches the database name (with `-` → `_`) -2. Verify the init script ran successfully: - ```bash - docker compose logs db | grep "Convex database" - ``` - -### Issue: "SSL connection required" - -**Solution**: Set `DO_NOT_REQUIRE_SSL=1` in your `.env` file for local development - -## Migration from SQLite - -If you're migrating from SQLite to PostgreSQL: - -1. Export your data: - - ```bash - npx convex export --path backup.zip - ``` - -2. Update environment variables to use PostgreSQL - -3. Restart the backend: - - ```bash - docker compose down - docker compose up -d - ``` - -4. Import your data: - ```bash - npx convex import --replace-all backup.zip - ``` - -## References - -- [Convex Self-Hosted Documentation](https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md) -- [Convex PostgreSQL Setup](https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md#connecting-to-postgres-on-neon) -- [Convex Environment Variables](https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md#optional-configurations) - -## Production Considerations - -For production deployments: - -1. **Use a managed PostgreSQL service** (e.g., AWS RDS, Google Cloud SQL, Neon) -2. **Enable SSL**: Remove `DO_NOT_REQUIRE_SSL` and configure SSL certificates -3. **Set a strong `INSTANCE_SECRET`**: Generate with `openssl rand -hex 32` -4. **Configure proper backup strategy**: Use PostgreSQL backup tools -5. **Monitor database performance**: Ensure backend and database are in the same region -6. **Set up proper authentication**: Configure admin keys securely - -## Database Schema - -Convex manages its own schema automatically. You don't need to create tables manually. The backend will create the necessary tables on first run: - -- `_tables`: Metadata about Convex tables -- `_documents`: Document storage -- `_indexes`: Index definitions -- `_modules`: Convex function modules -- And other internal tables - -## Performance Tips - -1. **Co-locate backend and database**: Deploy in the same region/datacenter -2. **Monitor query performance**: Use PostgreSQL's `pg_stat_statements` -3. **Adjust connection pool**: Configure based on your workload -4. **Regular maintenance**: Run `VACUUM` and `ANALYZE` periodically - -## Support - -For issues and questions: - -- [Convex Discord](https://discord.gg/convex) - `#self-hosted` channel -- [GitHub Issues](https://github.com/get-convex/convex-backend/issues) diff --git a/docs/deployment-modes.md b/docs/deployment-modes.md deleted file mode 100644 index a37374b3c8..0000000000 --- a/docs/deployment-modes.md +++ /dev/null @@ -1,164 +0,0 @@ -# Deployment Modes - -Tale supports two deployment modes: **Local Development** (without HTTPS) and **Production** (with HTTPS via Caddy proxy). - -## Local Development Mode (Default) - -**Use case:** Running Tale on your local machine for development or testing. - -### Setup - -1. Create `.env` file: - -```bash -DOMAIN=http://localhost -DB_NAME=tale -DB_USER=tale -DB_PASSWORD=change_me -OPENAI_API_KEY=sk-... -``` - -2. Start services: - -```bash -docker compose up --build -``` - -3. Access the platform: - -- **Main app:** http://localhost:3000 -- **Other services:** See README.md for port mappings - -### What happens - -- ✅ All services start EXCEPT the proxy -- ✅ Platform is directly accessible on port 3000 -- ✅ No SSL certificates needed -- ✅ Simple and fast for development - -## Production Mode (With HTTPS) - -**Use case:** Deploying Tale on a server with a domain name and HTTPS. - -### Setup - -1. Create `.env` file: - -```bash -DOMAIN=https://yourdomain.com -ACME_EMAIL=you@example.com -DB_NAME=tale -DB_USER=tale -DB_PASSWORD=change_me -OPENAI_API_KEY=sk-... -``` - -2. Start services with production profile: - -```bash -docker compose --profile production up -d --build -``` - -3. Access the platform: - -- **Main app:** https://yourdomain.com (via Caddy on ports 80/443) - -### What happens - -- ✅ All services start INCLUDING the proxy -- ✅ Caddy handles HTTPS termination -- ✅ Automatic SSL certificate from Let's Encrypt -- ✅ Auto-renewal of certificates -- ✅ HTTP→HTTPS redirect -- ✅ Security headers added - -### Prerequisites - -- Domain name pointing to your server's IP -- Ports 80 and 443 open in firewall -- Valid email for ACME registration - -## Switching Between Modes - -### From Local to Production - -1. Update `.env`: - -```bash -DOMAIN=https://yourdomain.com -ACME_EMAIL=you@example.com -``` - -2. Restart with production profile: - -```bash -docker compose down -docker compose --profile production up -d --build -``` - -### From Production to Local - -1. Update `.env`: - -```bash -DOMAIN=http://localhost -``` - -2. Restart without profile: - -```bash -docker compose down -docker compose up --build -``` - -## Architecture Differences - -### Local Mode - -``` -[Browser] → http://localhost:3000 → [Platform (Next.js)] - ↓ - [Convex Backend] - ↓ - [Other Services] -``` - -### Production Mode - -``` -[Browser] → https://yourdomain.com → [Proxy (Caddy)] → [Platform (Next.js)] - ↓ - [Convex Backend] - ↓ - [Other Services] -``` - -## Troubleshooting - -### Local Mode Issues - -**Problem:** Can't access http://localhost:3000 - -- Check if platform service is running: `docker ps | grep tale-platform` -- Check logs: `docker logs tale-platform` -- Verify port 3000 is not in use: `lsof -i :3000` - -### Production Mode Issues - -**Problem:** Proxy not starting - -- Ensure you're using the production profile: `docker compose --profile production up` -- Check proxy logs: `docker logs tale-proxy` - -**Problem:** SSL certificate errors - -- Verify domain DNS points to your server -- Check ACME_EMAIL is set in .env -- Try staging first: `ACME_CA=https://acme-staging-v02.api.letsencrypt.org/directory` -- Check ports 80/443 are open: `sudo netstat -tlnp | grep -E ':(80|443)'` - -**Problem:** "Connection refused" errors - -- Ensure platform service is healthy: `docker ps` (check STATUS column) -- Check platform logs: `docker logs tale-platform` -- Verify internal networking: `docker network inspect tale_internal` diff --git a/docs/email-api-sending.md b/docs/email-api-sending.md deleted file mode 100644 index 4936183577..0000000000 --- a/docs/email-api-sending.md +++ /dev/null @@ -1,289 +0,0 @@ -# Email API Sending (Gmail API & Microsoft Graph) - -This document explains how to use API-based email sending instead of SMTP to avoid port blocking issues on cloud providers like DigitalOcean. - -## Overview - -The system now supports two methods for sending emails: - -1. **SMTP** - Traditional email sending via SMTP protocol (ports 25, 465, 587) -2. **API** - Modern API-based sending via Gmail API or Microsoft Graph API (HTTPS only) - -## Why Use API Sending? - -- ✅ **No port blocking** - Uses HTTPS (port 443) which is never blocked -- ✅ **Better for cloud providers** - Works on DigitalOcean, AWS, etc. without special configuration -- ✅ **No DNS setup needed** - When sending from Gmail/Outlook accounts (e.g., `support@gmail.com`, `larry.luo@tale.dev`) -- ✅ **Full threading support** - Maintains email conversations with `In-Reply-To` and `References` headers -- ✅ **Message ID tracking** - Returns Internet Message ID for tracking and threading -- ✅ **OAuth2 security** - Uses secure OAuth2 tokens instead of passwords - -## How It Works - -### Architecture - -``` -Email Provider (with sendMethod: 'api') - ↓ -send_message_via_email.ts (checks sendMethod) - ↓ -sendMessageViaAPIInternal (routes to correct API) - ↓ -Gmail API or Microsoft Graph API - ↓ -Returns Internet Message ID for threading -``` - -### Supported Providers - -| Provider | API | OAuth2 Required | DNS Setup Required | -|----------|-----|-----------------|-------------------| -| Gmail | Gmail API | Yes | No (if sending from @gmail.com) | -| Outlook/Microsoft 365 | Microsoft Graph API | Yes | No (if domain already on M365) | - -## Setup Instructions - -### 1. Create Email Provider with API Sending - -When creating an email provider, set `sendMethod: 'api'`: - -```typescript -import { api } from '@/convex/_generated/api'; - -// Create Gmail provider with API sending -const providerId = await ctx.runAction(api.email_providers.createOAuth2Provider, { - organizationId: 'your-org-id', - name: 'Gmail API', - vendor: 'gmail', - sendMethod: 'api', // ← Use API instead of SMTP - oauth2Auth: { - provider: 'gmail', - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - authorizationCode: 'auth-code-from-oauth-flow', - }, - // No smtpConfig needed for API sending! - imapConfig: { - host: 'imap.gmail.com', - port: 993, - secure: true, - }, -}); -``` - -```typescript -// Create Outlook/Microsoft 365 provider with API sending -const providerId = await ctx.runAction(api.email_providers.createOAuth2Provider, { - organizationId: 'your-org-id', - name: 'Outlook API', - vendor: 'outlook', - sendMethod: 'api', // ← Use API instead of SMTP - oauth2Auth: { - provider: 'microsoft', - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - authorizationCode: 'auth-code-from-oauth-flow', - accountType: 'organizational', // or 'personal' - }, - // No smtpConfig needed for API sending! - imapConfig: { - host: 'outlook.office365.com', - port: 993, - secure: true, - }, -}); -``` - -### 2. OAuth2 Scopes Required - -**Gmail:** -- `https://mail.google.com/` - Full Gmail access (send + read) - -**Microsoft:** -- `https://graph.microsoft.com/Mail.Send` - Send emails -- `https://outlook.office.com/IMAP.AccessAsUser.All` - IMAP access (for receiving) -- `https://graph.microsoft.com/User.Read` - Get user info -- `offline_access` - Refresh tokens - -### 3. Sending Emails - -Once configured, sending works exactly the same as before: - -```typescript -import { api } from '@/convex/_generated/api'; - -// Send email (automatically uses API if sendMethod is 'api') -await ctx.runMutation(api.conversations.sendMessage, { - conversationId: 'conversation-id', - organizationId: 'your-org-id', - content: 'Email body', - to: ['recipient@example.com'], - subject: 'Hello from API', - html: '

Email body

', - // Threading headers work the same - inReplyTo: '', - references: ['', ''], -}); -``` - -## Features - -### ✅ Message ID Tracking - -Both Gmail API and Microsoft Graph API return the Internet Message ID: - -```typescript -// After sending, the message is updated with: -{ - externalMessageId: '', // Internet Message ID - deliveryState: 'sent', - sentAt: 1234567890, -} -``` - -### ✅ Email Threading - -Threading works exactly like SMTP: - -```typescript -// Reply to an existing email -await ctx.runMutation(api.conversations.sendMessage, { - conversationId: 'conversation-id', - organizationId: 'your-org-id', - content: 'Reply message', - to: ['recipient@example.com'], - subject: 'Re: Original Subject', - html: '

Reply message

', - inReplyTo: '', - references: ['', ''], -}); -``` - -### ✅ HTML and Plain Text - -Both content types are supported: - -```typescript -await ctx.runMutation(api.conversations.sendMessage, { - // ... other fields - html: '

Hello

This is HTML

', - text: 'Hello\n\nThis is plain text', -}); -``` - -### ✅ CC, BCC, Reply-To - -All standard email fields work: - -```typescript -await ctx.runMutation(api.conversations.sendMessage, { - // ... other fields - to: ['recipient@example.com'], - cc: ['cc@example.com'], - bcc: ['bcc@example.com'], - replyTo: 'reply-to@example.com', -}); -``` - -## Migration from SMTP to API - -### Option 1: Update Existing Provider - -```typescript -// Update an existing provider to use API -await ctx.runMutation(api.email_providers.update, { - providerId: 'existing-provider-id', - sendMethod: 'api', // Switch from SMTP to API - // Remove smtpConfig if no longer needed - smtpConfig: undefined, -}); -``` - -### Option 2: Create New Provider - -Create a new provider with `sendMethod: 'api'` and set it as default. - -## Troubleshooting - -### Error: "API sending requires OAuth2 authentication" - -**Solution:** API sending only works with OAuth2. Password authentication is not supported for API sending. - -### Error: "Unsupported provider for API sending" - -**Solution:** Only Gmail and Microsoft providers support API sending. For other providers, use SMTP. - -### Error: "Gmail API error: 401 Unauthorized" - -**Solution:** OAuth2 token expired. The system should automatically refresh it. If not, re-authorize the provider. - -### Error: "Microsoft Graph API error: 403 Forbidden" - -**Solution:** Check that the OAuth2 app has the required scopes (`Mail.Send`). - -## Comparison: SMTP vs API - -| Feature | SMTP | API | -|---------|------|-----| -| **Port blocking** | ❌ Often blocked | ✅ Never blocked (HTTPS) | -| **DNS setup** | ⚠️ Required for custom domains | ✅ Not needed for Gmail/M365 | -| **OAuth2 support** | ✅ Yes (XOAUTH2) | ✅ Yes (native) | -| **Password auth** | ✅ Yes | ❌ No | -| **Threading** | ✅ Yes | ✅ Yes | -| **Message ID** | ✅ Yes | ✅ Yes | -| **Attachments** | ✅ Yes | ⚠️ Not yet implemented | -| **Rate limits** | ⚠️ Provider-specific | ✅ Higher limits | - -## Best Practices - -1. **Use API for cloud deployments** - Avoid port blocking issues -2. **Use SMTP for on-premise** - If you control the network -3. **Keep OAuth2 tokens fresh** - System handles this automatically -4. **Monitor token expiry** - Check provider status regularly -5. **Test before production** - Send test emails to verify configuration - -## Example: Complete Setup - -```typescript -// 1. Create OAuth2 provider with API sending -const providerId = await ctx.runAction(api.email_providers.createOAuth2Provider, { - organizationId: 'org-123', - name: 'Company Outlook', - vendor: 'outlook', - sendMethod: 'api', - oauth2Auth: { - provider: 'microsoft', - clientId: process.env.MICROSOFT_CLIENT_ID, - clientSecret: process.env.MICROSOFT_CLIENT_SECRET, - authorizationCode: authCode, - accountType: 'organizational', - }, - imapConfig: { - host: 'outlook.office365.com', - port: 993, - secure: true, - }, -}); - -// 2. Send email (automatically uses API) -const messageId = await ctx.runMutation(api.conversations.sendMessage, { - conversationId: conversationId, - organizationId: 'org-123', - providerId: providerId, - content: 'Hello from API!', - to: ['customer@example.com'], - subject: 'Welcome', - html: '

Welcome!

Thanks for signing up.

', -}); - -// 3. Message is sent via Microsoft Graph API -// 4. Internet Message ID is stored for threading -``` - -## Next Steps - -- [ ] Add attachment support for API sending -- [ ] Add delivery status tracking via webhooks -- [ ] Add read receipt support -- [ ] Add batch sending optimization - diff --git a/docs/email-providers.md b/docs/email-providers.md deleted file mode 100644 index 35da1a1ee5..0000000000 --- a/docs/email-providers.md +++ /dev/null @@ -1,659 +0,0 @@ -# Email Providers – Convex Integration Guide - -A single, concise guide for developers to understand and integrate the email providers feature using Convex. English-only. Follows Convex best practices. - -## What you need to know - -- Providers supported: Resend (fallback), SMTP (send), IMAP (receive), OAuth2 (Gmail, Microsoft), Password auth -- If both SMTP and IMAP are configured, both are validated and used -- Local dev uses Inbucket (see Local Development Setup section below) -- All data stored in Convex with proper encryption for secrets -- Uses Convex mutations for writes and queries for reads -- **Automatic default**: When creating the first email provider for an organization, it will automatically be set as the default provider, even if `isDefault: false` is specified - -## Convex Schema - -The `emailProviders` table is defined in `convex/schema.ts`: - -```typescript -emailProviders: defineTable({ - organizationId: v.id('organizations'), - name: v.string(), - vendor: v.union( - v.literal('gmail'), - v.literal('outlook'), - v.literal('smtp'), - v.literal('resend'), - v.literal('other'), - ), - authMethod: v.union( - v.literal('password'), - v.literal('oauth2'), - ), - - // Auth configurations (encrypted in metadata) - passwordAuth: v.optional(v.object({ - user: v.string(), - passEncrypted: v.string(), // encrypted password - })), - - oauth2Auth: v.optional(v.object({ - provider: v.string(), // 'gmail' | 'microsoft' - clientId: v.string(), - clientSecretEncrypted: v.string(), // encrypted secret - accessTokenEncrypted: v.optional(v.string()), - refreshTokenEncrypted: v.optional(v.string()), - tokenExpiry: v.optional(v.number()), - })), - - // SMTP configuration - smtpConfig: v.optional(v.object({ - host: v.string(), - port: v.number(), - secure: v.boolean(), - })), - - // IMAP configuration - imapConfig: v.optional(v.object({ - host: v.string(), - port: v.number(), - secure: v.boolean(), - })), - - // Status and metadata - isActive: v.boolean(), - isDefault: v.boolean(), - status: v.optional(v.union( - v.literal('active'), - v.literal('error'), - v.literal('testing'), - )), - lastTestedAt: v.optional(v.number()), - lastSyncAt: v.optional(v.number()), - errorMessage: v.optional(v.string()), - - metadata: v.optional(v.any()), -}) - .index('by_organizationId', ['organizationId']) - .index('by_organizationId_and_vendor', ['organizationId', 'vendor']) - .index('by_organizationId_and_isDefault', ['organizationId', 'isDefault']) - .index('by_organizationId_and_status', ['organizationId', 'status']), -``` - -## UI flows (recommended) - -1. Providers list page (simplified) - -- Show table of providers (name, vendor, status, default, last tested) -- Buttons: Create, Set Default, Test, Edit, Delete - -2. Create provider page (dedicated) - -- Tabs are not required; use a single page with radio for Auth Method: Password or OAuth2 -- Show vendor presets (Gmail/Outlook) to prefill hostnames/ports -- If OAuth2 + Microsoft (use existing login) is selected, we redirect to consent - -3. Test provider action - -- Call mutation to test; show SMTP and IMAP results separately - -4. Receive email sync - -- Manual "Sync now" triggers action; show results (processed/total/errors) - -## Convex Functions (import paths) - -All functions are in `convex/emailProviders.ts`: - -### Queries - -```typescript -import { api } from '@/convex/_generated/api'; - -// List all providers for an organization -api.emailProviders.list; - -// Get single provider by ID -api.emailProviders.get; - -// Get default provider for an organization -api.emailProviders.getDefault; -``` - -### Mutations - -```typescript -// Update provider configuration -api.emailProviders.update; - -// Delete provider -api.emailProviders.delete; - -// Set provider as default -api.emailProviders.setDefault; -``` - -### Actions - -```typescript -// Create email provider (password or OAuth2) - with encryption -api.emailProviders.create; - -// Test SMTP/IMAP connectivity -api.emailProviders.test; - -// Send email using provider -api.emailProviders.sendEmail; - -// Sync received emails via IMAP -api.emailProviders.syncEmails; - -// Initialize OAuth2 flow (returns auth URL) -api.emailProviders.initOAuth2; - -// Handle OAuth2 callback -api.emailProviders.handleOAuth2Callback; -``` - -## Quick start – examples - -### Using from React components - -```typescript -'use client'; - -import { useQuery, useMutation, useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; -import type { Id } from '@/convex/_generated/dataModel'; - -function EmailProvidersPage({ organizationId }: { organizationId: Id<'organizations'> }) { - // List providers - const providers = useQuery(api.emailProviders.list, { organizationId }); - - // Create provider action (handles encryption) - const createProvider = useAction(api.emailProviders.create); - - // Handle create - const handleCreate = async () => { - await createProvider({ - organizationId, - name: 'Gmail (App Password)', - vendor: 'gmail', - authMethod: 'password', - passwordAuth: { - user: 'you@gmail.com', - pass: 'app-password' // will be encrypted - }, - smtpConfig: { host: 'smtp.gmail.com', port: 587, secure: false }, - imapConfig: { host: 'imap.gmail.com', port: 993, secure: true }, - isActive: true, - isDefault: true, - }); - }; - - return ( -
- {providers?.map(provider => ( -
{provider.name}
- ))} - -
- ); -} -``` - -### Create provider (Gmail + App Password) - -```typescript -import { useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const createProvider = useAction(api.emailProviders.create); - -await createProvider({ - organizationId, - name: 'Gmail (App Password)', - vendor: 'gmail', - authMethod: 'password', - passwordAuth: { - user: 'you@gmail.com', - pass: 'app-password', // automatically encrypted by action - }, - smtpConfig: { host: 'smtp.gmail.com', port: 587, secure: false }, - imapConfig: { host: 'imap.gmail.com', port: 993, secure: true }, - isActive: true, - isDefault: true, -}); -``` - -### Create provider (Gmail OAuth2) - -```typescript -import { useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const initOAuth2 = useAction(api.emailProviders.initOAuth2); - -const authUrl = await initOAuth2({ - organizationId, - name: 'Gmail OAuth2', - vendor: 'gmail', - authMethod: 'oauth2', - oauth2Auth: { - provider: 'gmail', - clientId: 'your-client-id', - clientSecret: 'your-secret', // will be encrypted - }, - isActive: true, - isDefault: true, -}); - -if (authUrl) { - window.location.href = authUrl; -} -``` - -### Get default provider - -```typescript -import { useQuery } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const defaultProvider = useQuery(api.emailProviders.getDefault, { - organizationId, -}); -``` - -### Test provider - -```typescript -import { useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const testProvider = useAction(api.emailProviders.test); - -const result = await testProvider({ providerId }); -if (result.success) { - console.log('SMTP:', result.smtp); // { success: true, latencyMs: 123 } - console.log('IMAP:', result.imap); // { success: true, latencyMs: 45 } -} -``` - -### Send email - -```typescript -import { useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const sendEmail = useAction(api.emailProviders.sendEmail); - -await sendEmail({ - organizationId, - to: ['recipient@example.com'], - subject: 'Hello', - html: '

Hi

', - // Optional: specify provider, otherwise uses default - providerId: myProviderId, -}); -``` - -### Sync received emails - -```typescript -import { useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const syncEmails = useAction(api.emailProviders.syncEmails); - -const result = await syncEmails({ - organizationId, - limit: 50, -}); -console.log(`Synced ${result.processed} of ${result.total} emails`); -``` - -## OAuth2 Integration - -### OAuth2 Flow - -1. Call `api.emailProviders.initOAuth2` action to start the flow -2. User is redirected to provider consent screen -3. After consent, user is redirected back to `/api/auth/oauth2/callback` -4. Callback handler calls `api.emailProviders.handleOAuth2Callback` to complete setup -5. Tokens are encrypted and stored in the `emailProviders` table - -### Redirect Configuration - -- OAuth2 callback route: `/api/auth/oauth2/callback` -- Configure this exact URL in your OAuth2 provider console (Gmail/Microsoft) -- Ensure `SITE_URL` is set correctly in `.env` (or `DOMAIN` in Docker Compose) - -### Required Environment Variables - -```bash -# Base URL for OAuth2 callbacks (set via DOMAIN in Docker Compose) -SITE_URL=http://localhost:3000 - -# For Microsoft existing account flow (optional) -AUTH_MICROSOFT_ENTRA_ID_ID=your-client-id -AUTH_MICROSOFT_ENTRA_ID_SECRET=your-client-secret -AUTH_MICROSOFT_ENTRA_ID_ISSUER=https://login.microsoftonline.com/common/v2.0 -``` - -### Scopes and Prerequisites - -**Gmail** - -- Scope: `https://mail.google.com/` (covers SMTP/IMAP/POP via XOAUTH2) -- You do NOT need `gmail.send` or `gmail.readonly` unless you call the Gmail REST API -- We request refresh tokens with `access_type=offline` and `prompt=consent` -- Enable IMAP in Gmail settings - -**Microsoft 365 / Outlook** - -- Scopes: `https://outlook.office.com/SMTP.Send`, `https://outlook.office.com/IMAP.AccessAsUser.All`, `offline_access` -- Ensure IMAP and SMTP AUTH are enabled in the tenant and for the mailbox in Exchange Online -- Choose tenant endpoint based on account type: consumers, organizations, or common - -### OAuth2 Token Management - -- Tokens are encrypted using Convex encryption utilities before storage -- Stored in the `oauth2Auth` field of the `emailProviders` table -- Automatic token refresh is handled by scheduled Convex cron jobs -- Token expiry is tracked in the `tokenExpiry` field (Unix timestamp) - -### Implementation Details - -**Convex Files** - -- `convex/emailProviders.ts` – main queries, mutations, and actions -- `convex/lib/email-oauth2.ts` – OAuth2 utilities (token exchange, refresh, encryption) -- `convex/crons.ts` – scheduled token refresh job -- `app/api/auth/oauth2/callback/route.ts` – Next.js route handler for OAuth2 callbacks - -**Token Refresh Strategy** - -- Cron job runs every 30 minutes -- Checks for tokens expiring in the next hour -- Refreshes tokens proactively before expiry -- Updates `tokenExpiry` and `accessTokenEncrypted` fields - -## Convex Best Practices for Email Providers - -### Query Patterns - -```typescript -// ✅ Good: Use indexed query for performance -const providers = await ctx.db - .query('emailProviders') - .withIndex('by_organizationId', (q) => q.eq('organizationId', organizationId)) - .filter((q) => q.eq(q.field('isActive'), true)) - .collect(); - -// ❌ Bad: Don't use filter without index -const providers = await ctx.db - .query('emailProviders') - .filter((q) => q.eq(q.field('organizationId'), organizationId)) - .collect(); -``` - -### Mutation Patterns - -```typescript -// ✅ Good: Validate and encrypt secrets -export const create = mutation({ - args: { - organizationId: v.id('organizations'), - name: v.string(), - passwordAuth: v.optional( - v.object({ - user: v.string(), - pass: v.string(), // will be encrypted - }), - ), - // ... other args - }, - handler: async (ctx, args) => { - // Encrypt password before storage - const passEncrypted = args.passwordAuth?.pass - ? await encrypt(args.passwordAuth.pass) - : undefined; - - return await ctx.db.insert('emailProviders', { - ...args, - passwordAuth: passEncrypted - ? { - user: args.passwordAuth!.user, - passEncrypted, - } - : undefined, - }); - }, -}); -``` - -### Action Patterns - -```typescript -// ✅ Good: Use actions for external API calls (SMTP/IMAP) -export const test = action({ - args: { providerId: v.id('emailProviders') }, - returns: v.object({ - success: v.boolean(), - smtp: v.optional( - v.object({ - success: v.boolean(), - latencyMs: v.number(), - error: v.optional(v.string()), - }), - ), - imap: v.optional( - v.object({ - success: v.boolean(), - latencyMs: v.number(), - error: v.optional(v.string()), - }), - ), - }), - handler: async (ctx, args) => { - // Get provider data via query - const provider = await ctx.runQuery(internal.emailProviders.getInternal, { - providerId: args.providerId, - }); - - // Test SMTP/IMAP connections (external API calls) - const smtpResult = await testSmtpConnection(provider); - const imapResult = await testImapConnection(provider); - - // Update provider status via mutation - await ctx.runMutation(internal.emailProviders.updateStatus, { - providerId: args.providerId, - status: smtpResult.success && imapResult.success ? 'active' : 'error', - lastTestedAt: Date.now(), - }); - - return { - success: smtpResult.success && imapResult.success, - smtp: smtpResult, - imap: imapResult, - }; - }, -}); -``` - -## Troubleshooting - -### Common Issues - -**Gmail password auth fails** - -- Ensure App Passwords are enabled (requires 2FA) -- Consider migrating to OAuth2 for better security -- Verify IMAP is enabled in Gmail settings - -**OAuth2 authorization errors** - -- Verify scopes match exactly in provider console -- Ensure redirect URI matches exactly (including protocol and port) -- Check that `SITE_URL` is configured correctly (or `DOMAIN` in Docker Compose) - -**Missing refresh tokens** - -- Gmail: Ensure `access_type=offline` and `prompt=consent` are set -- Microsoft: Ensure `offline_access` scope is included - -**SMTP/IMAP connection errors** - -- Gmail: `smtp.gmail.com:587` (SMTP), `imap.gmail.com:993` (IMAP) -- Outlook: `smtp.office365.com:587` (SMTP), `outlook.office365.com:993` (IMAP) -- Verify firewall rules and network connectivity -- Check provider rate limits - -**Slow SMTP performance** - -- Increase connection/greeting/socket timeouts -- Consider connection pooling for high-volume scenarios - -**TLS issues (development only)** - -- For local dev, you may set `tls.rejectUnauthorized=false` -- NEVER use this in production - -### Migration from Password to OAuth2 - -1. Create a new OAuth2 provider for the same mailbox -2. Complete OAuth2 consent flow -3. Test the new provider -4. Set new provider as default: `api.emailProviders.setDefault` -5. Deactivate the old password-based provider -6. Optionally delete the old provider after confirming the new one works - -## UI Implementation Guidelines - -### Provider List Component - -```typescript -'use client'; - -import { useQuery, useMutation, useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; - -export function EmailProvidersList({ organizationId }) { - const providers = useQuery(api.emailProviders.list, { organizationId }); - const deleteProvider = useMutation(api.emailProviders.delete); - const setDefault = useMutation(api.emailProviders.setDefault); - const testProvider = useAction(api.emailProviders.test); - - return ( - - - - Name - Vendor - Status - Default - Actions - - - - {providers?.map(provider => ( - - {provider.name} - {provider.vendor} - - - {provider.status} - - - {provider.isDefault ? 'Yes' : 'No'} - - - - - - - ))} - -
- ); -} -``` - -### Key UI Principles - -- **Mask secrets**: Never render passwords or tokens in the UI -- **Show separate results**: Display SMTP and IMAP test results independently -- **Real-time updates**: Use Convex's reactive queries for live status updates -- **Error handling**: Show clear error messages for failed operations -- **Loading states**: Use Convex's loading states for better UX -- **Sync button**: Offer a "Sync now" button with result summary - -## Local Development Setup - -For local development, use Inbucket as a local SMTP server instead of real email services. - -### Quick Setup - -1. **Install Inbucket:** - -```bash -# Using Docker (recommended) -docker run -d -p 9000:9000 -p 2500:2500 --name inbucket inbucket/inbucket - -# Using Homebrew (macOS) -brew install inbucket && inbucket - -# Using Go -go install github.com/inbucket/inbucket@latest && inbucket -``` - -2. **Create local provider:** - -```typescript -import { useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -const createProvider = useAction(api.emailProviders.create); - -await createProvider({ - organizationId, - name: 'Local Dev (Inbucket)', - vendor: 'smtp', - authMethod: 'password', - passwordAuth: { - user: 'dev@local', - pass: 'any', // Inbucket accepts any password (automatically encrypted by action) - }, - smtpConfig: { - host: 'localhost', - port: 2500, - secure: false, - }, - isActive: true, - isDefault: true, -}); -``` - -3. **View emails:** Open http://localhost:9000 - -That's it! No special configuration needed - it's just another email provider pointing to localhost. - -**Pro tip:** Use different Convex deployments for dev and production. Each deployment has its own `emailProviders` table, so your local Inbucket provider stays in dev only. - -## Additional Resources - -- [Convex Schema Documentation](https://docs.convex.dev/database/schemas) -- [Convex Queries and Mutations](https://docs.convex.dev/functions) -- [Convex Actions](https://docs.convex.dev/functions/actions) -- [Gmail OAuth2 Setup](https://developers.google.com/identity/protocols/oauth2) -- [Microsoft Graph OAuth2](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) -- [Inbucket Email Testing](https://www.inbucket.org/) diff --git a/docs/integration-health-checks.md b/docs/integration-health-checks.md deleted file mode 100644 index e70cfa8268..0000000000 --- a/docs/integration-health-checks.md +++ /dev/null @@ -1,199 +0,0 @@ -# Integration Health Checks - -## Overview - -This document describes the health check system implemented for Shopify and Circuly integrations. Health checks are automatically run when creating or updating integrations to ensure credentials are valid before saving them to the database. - -**Note:** Gmail and Outlook are managed separately through the `emailProviders` system and do not use the `integrations` table. They have their own authentication flow using OAuth2. - -## Implementation - -### Key Features - -1. **Automatic Health Checks on Create**: When creating a new Shopify or Circuly integration, the credentials are validated against the actual API before the integration is saved. - -2. **Automatic Health Checks on Update**: When updating credentials for an existing integration, the new credentials are validated before being saved. - -3. **Fail-Fast Behavior**: If a health check fails, an error is thrown and the integration is NOT created/updated. This prevents invalid integrations from being stored. - -4. **User-Friendly Error Messages**: Detailed error messages help users understand what went wrong: - - "Shopify authentication failed. Please check your access token." - - "Shopify store not found. Please verify your domain." - - "Circuly authentication failed. Please check your username and password." - -5. **Active Status on Success**: When a health check passes during creation, the integration is automatically set to `status: 'active'` and `isActive: true`. - -### Health Check Functions - -#### `testShopifyConnection(domain, accessToken)` - -Tests a Shopify connection by calling the `/admin/api/2024-01/shop.json` endpoint. - -**Validates:** - -- Access token is valid -- Store domain exists -- API permissions are correct - -**Error Codes:** - -- 401: Invalid access token -- 403: Insufficient permissions -- 404: Store not found - -#### `testCirculyConnection(username, password)` - -Tests a Circuly connection by calling the `/v1/account` endpoint with basic authentication. - -**Validates:** - -- Username and password are correct -- Account has proper permissions - -**Error Codes:** - -- 401: Invalid credentials -- 403: Insufficient permissions - -### Integration Points - -The health checks are integrated into three key functions: - -1. **`create` action**: Runs health check before creating a new integration -2. **`update` action**: Runs health check when credentials are being updated -3. **`testConnection` action**: Uses the same health check functions for manual testing - -## Usage - -### Creating an Integration - -```typescript -// Shopify -const integrationId = await ctx.runAction(api.integrations.create, { - organizationId: 'org123', - provider: 'shopify', - authMethod: 'api_key', - apiKeyAuth: { - key: 'shpat_abc123...', - keyPrefix: 'shpat_', - }, - connectionConfig: { - domain: 'mystore.myshopify.com', - }, -}); -// If health check fails, an error is thrown and no integration is created - -// Circuly -const integrationId = await ctx.runAction(api.integrations.create, { - organizationId: 'org123', - provider: 'circuly', - authMethod: 'basic_auth', - basicAuth: { - username: 'myusername', - password: 'mypassword', - }, -}); -// If health check fails, an error is thrown and no integration is created -``` - -### Updating Credentials - -```typescript -await ctx.runAction(api.integrations.update, { - integrationId: 'int123', - apiKeyAuth: { - key: 'new_token_here', - }, - connectionConfig: { - domain: 'newstore.myshopify.com', - }, -}); -// Health check runs automatically when credentials are changed -``` - -### Manual Testing - -```typescript -const result = await ctx.runAction(api.integrations.testConnection, { - integrationId: 'int123', -}); -// Returns { success: boolean, message: string } -``` - -## Error Handling - -When a health check fails: - -1. An error is thrown with a descriptive message -2. The integration is NOT created or updated -3. The error propagates to the UI where it's displayed to the user -4. For existing integrations being updated, the old credentials remain unchanged - -## Logging - -Health checks log their progress: - -``` -[Integration Create] Running health check for shopify... -[Shopify Health Check] Successfully connected to My Store Name -[Integration Create] Health check passed for shopify -[Integration Create] Successfully created shopify integration with ID: xyz -``` - -Failed health checks log errors: - -``` -[Integration Create] Running health check for shopify... -[Integration Create] Health check failed for shopify: Shopify authentication failed. Please check your access token. -``` - -## Architecture Notes - -### Integration Types - -**Integrations Table (with Health Checks):** - -- Shopify - API key authentication -- Circuly - Basic authentication - -**Email Providers Table (OAuth2):** - -- Gmail - OAuth2 authentication -- Outlook - OAuth2 authentication -- SMTP/IMAP - Custom configurations - -Email providers use a separate system (`emailProviders` table) because they have different authentication requirements (OAuth2), connection patterns (SMTP/IMAP), and use cases (email sending/receiving). - -## Future Enhancements - -Potential improvements: - -1. Implement periodic health checks to detect stale credentials -2. Add retry logic with exponential backoff -3. Store health check history for debugging -4. Add more granular permission checks -5. Add webhook verification for supported integrations - -## Related Files - -**Integration System:** - -- `/convex/integrations.ts` - Main integration management file with health checks -- `/convex/schema.ts` - Integration table schema (Shopify, Circuly) -- `/app/(app)/dashboard/[id]/settings/integrations/components/shopify-integration-dialog.tsx` - Shopify UI -- `/app/(app)/dashboard/[id]/settings/integrations/components/circuly-integration-dialog.tsx` - Circuly UI - -**Email Provider System:** - -- `/convex/emailProviders.ts` - Email provider management (Gmail, Outlook, SMTP) -- `/app/(app)/dashboard/[id]/settings/integrations/components/gmail-integration-dialog.tsx` - Gmail UI -- `/app/(app)/dashboard/[id]/settings/integrations/components/outlook-integration-dialog.tsx` - Outlook UI - -## Testing - -To test the health check system: - -1. **Valid Credentials**: Create an integration with valid credentials - should succeed -2. **Invalid Token**: Use an invalid access token - should fail with authentication error -3. **Invalid Domain**: Use a non-existent domain - should fail with store not found error -4. **Network Issues**: Simulate network failures to ensure proper error handling diff --git a/docs/integrations-schema.md b/docs/integrations-schema.md deleted file mode 100644 index a587d726ab..0000000000 --- a/docs/integrations-schema.md +++ /dev/null @@ -1,315 +0,0 @@ -# Integrations Schema Documentation - -## Overview - -The `integrations` table has been redesigned from a generic, unstructured table to a fully-typed, structured system for managing third-party service connections (Shopify, Circuly, Stripe, etc.). - -## Key Improvements - -### Before - -```typescript -integrations: defineTable({ - organizationId: v.id('organizations'), - provider: v.string(), // Too generic - status: v.optional(v.string()), // Too generic - metadata: v.optional(v.any()), // Everything in unstructured metadata -}); -``` - -### After - -```typescript -integrations: defineTable({ - organizationId: v.id('organizations'), - - // Structured provider identification - provider: v.union( - v.literal('shopify'), - v.literal('circuly'), - v.literal('stripe'), - v.literal('firmhouse'), - ), - name: v.string(), - - // Typed status - status: v.union( - v.literal('active'), - v.literal('inactive'), - v.literal('error'), - v.literal('testing'), - ), - - // Structured authentication - authMethod: v.union( - v.literal('api_key'), - v.literal('bearer_token'), - v.literal('basic_auth'), - v.literal('oauth2'), - ), - - // Encrypted credentials (like emailProviders pattern) - apiKeyAuth: v.optional({ keyEncrypted, keyPrefix }), - basicAuth: v.optional({ username, passwordEncrypted }), - oauth2Auth: v.optional({ accessTokenEncrypted, ... }), - - // Provider-specific config (structured, not v.any()) - connectionConfig: v.optional({ - domain, apiVersion, apiEndpoint, timeout, rateLimit - }), - - // Health tracking - lastSyncedAt, lastTestedAt, lastSuccessAt, lastErrorAt, errorMessage, - - // Sync statistics - syncStats: { totalRecords, lastSyncCount, failedSyncCount }, - - // Capabilities - capabilities: { canSync, canPush, canWebhook, syncFrequency }, - - // Only truly unstructured data - metadata: v.optional(v.any()), -}) -``` - -## Benefits - -### 1. Type Safety - -- IDE autocomplete for all fields -- Compile-time validation -- No runtime surprises - -### 2. Security - -- Credentials encrypted at rest (follows `emailProviders` pattern) -- Clear separation between config and secrets -- Audit trail with timestamps - -### 3. Observability - -- Health metrics (lastSuccessAt, lastErrorAt) -- Sync statistics -- Error messages - -### 4. Flexibility - -- Provider-specific config in structured object -- Capabilities flag for feature toggles -- Still extensible via `metadata` - -## Usage Examples - -### Creating a Shopify Integration - -```typescript -const integrationId = await ctx.runAction(api.integrations.create, { - organizationId: args.organizationId, - provider: 'shopify', - name: 'Main Store', - authMethod: 'api_key', - apiKeyAuth: { - key: 'shpat_1234...', // Will be encrypted - keyPrefix: 'shpat_', - }, - connectionConfig: { - domain: 'mystore.myshopify.com', - apiVersion: '2024-01', - timeout: 30000, - }, - capabilities: { - canSync: true, - canPush: false, - canWebhook: true, - syncFrequency: 'hourly', - }, -}); -``` - -### Creating a Circuly Integration - -```typescript -const integrationId = await ctx.runAction(api.integrations.create, { - organizationId: args.organizationId, - provider: 'circuly', - name: 'Circuly Subscriptions', - authMethod: 'bearer_token', - apiKeyAuth: { - key: apiKey, // Will be encrypted - }, - connectionConfig: { - apiEndpoint: 'https://api.circuly.io/api/2025-01', - }, - capabilities: { - canSync: true, - canPush: false, - syncFrequency: 'daily', - }, -}); -``` - -### Querying Integrations - -```typescript -// Get all integrations for an organization -const integrations = await ctx.runQuery(api.integrations.list, { - organizationId, -}); - -// Get specific provider integration -const shopifyIntegration = await ctx.runQuery(api.integrations.getByProvider, { - organizationId, - provider: 'shopify', -}); - -// Get decrypted credentials (for actions) -const credentials = await ctx.runAction( - api.integrations.getDecryptedCredentials, - { - integrationId, - } -); -// Returns: { apiKey, domain, connectionConfig, ... } -``` - -### Testing Connection - -```typescript -const result = await ctx.runAction(api.integrations.testConnection, { - integrationId, -}); - -if (result.success) { - // Integration is active and working - console.log(result.message); -} else { - // Integration has errors - console.error(result.message); -} -``` - -### Updating Sync Stats - -```typescript -await ctx.runMutation(api.integrations.updateSyncStats, { - integrationId, - totalRecords: 1500, - lastSyncCount: 150, - failedSyncCount: 2, -}); -``` - -## API Reference - -### Queries - -- `list(organizationId, provider?)` - List integrations -- `get(integrationId)` - Get single integration -- `getByProvider(organizationId, provider)` - Get by provider - -### Actions - -- `create(...)` - Create integration with encryption -- `update(integrationId, ...)` - Update integration -- `testConnection(integrationId)` - Test integration -- `getDecryptedCredentials(integrationId)` - Get decrypted creds - -### Mutations - -- `deleteIntegration(integrationId)` - Delete integration -- `updateSyncStats(integrationId, ...)` - Update sync stats - -## Integration with Workflows - -The Shopify workflow action now supports loading credentials from the integrations table: - -### Priority Order - -1. Direct parameters (`domain`, `accessToken`) -2. `variables.shopify` -3. `variables.workflow.shopify` -4. Integration table (via `shopifyIntegrationId` or `organizationId`) - -### Example Workflow Usage - -```typescript -// Option 1: Direct parameters (existing) -{ - domain: 'mystore.myshopify.com', - accessToken: 'shpat_...', - resource: 'products', -} - -// Option 2: Via variables (existing) -variables: { - shopify: { - domain: 'mystore.myshopify.com', - accessToken: 'shpat_...', - } -} - -// Option 3: From integration table (new - TODO) -variables: { - shopifyIntegrationId: 'j1234...', // Load from specific integration - // OR - organizationId: 'k5678...', // Auto-find Shopify integration -} -``` - -## Security Considerations - -### Encryption - -- All credentials encrypted using `oauth2.encrypt/decrypt` actions -- Same pattern as `emailProviders` -- Encryption keys stored in environment variables - -### Access Control - -- Integration credentials only decrypted in actions -- Mutations work with encrypted values -- Queries never return decrypted credentials - -### Audit Trail - -- `lastTestedAt` - When connection was last tested -- `lastSyncedAt` - When last sync occurred -- `lastSuccessAt` / `lastErrorAt` - Success/failure tracking -- `errorMessage` - Last error details - -## Migration Path - -For existing systems using variables for credentials: - -1. **Phase 1**: Keep using variables (fully backward compatible) -2. **Phase 2**: Create integrations through UI -3. **Phase 3**: Workflows automatically use integration table as fallback -4. **Phase 4**: Remove inline credentials from variables - -## Comparison with EmailProviders - -| Feature | emailProviders | integrations | -| --------------------- | ------------------------- | ---------------------------------- | -| Schema Structure | ✅ Fully structured | ✅ Fully structured | -| Encryption | ✅ Encrypted credentials | ✅ Encrypted credentials | -| Multiple Auth Methods | ✅ Password, OAuth2, SMTP | ✅ API Key, Basic, OAuth2 | -| Health Tracking | ✅ Status, last tested | ✅ Status, last tested, sync stats | -| Provider Config | ✅ SMTP/IMAP config | ✅ Connection config | -| Multiple per Org | ✅ Multiple providers | ✅ Multiple providers | - -## Next Steps - -1. ✅ Schema updated -2. ✅ Backend API created (`convex/integrations.ts`) -3. ✅ Shopify workflow action updated -4. 🔄 UI integration handlers (placeholder TODOs) -5. ⏳ Actual connection testing implementation -6. ⏳ Sync job integration -7. ⏳ Webhook support - -## Files Changed - -- `convex/schema.ts` - Updated integrations table -- `convex/integrations.ts` - New integration management API -- `convex/workflow/nodes/action/actions/shopify.ts` - Updated to support integration lookup -- `app/(app)/dashboard/[id]/settings/integrations/integrations.tsx` - Placeholder handlers diff --git a/docs/management-dashboard-user-guide.md b/docs/management-dashboard-user-guide.md deleted file mode 100644 index 89e2d999e8..0000000000 --- a/docs/management-dashboard-user-guide.md +++ /dev/null @@ -1,289 +0,0 @@ -# Management Dashboard User Guide - -## Overview - -This documentation provides operations staff with a comprehensive guide to understanding and using the management dashboard system's features and capabilities. The dashboard serves as a central hub for managing customer relationships, product recommendations, automated communications, and business insights. - -## System Architecture - -The management dashboard features a modern interface designed to streamline business operations through integrated modules: - -- **Home Dashboard** - Central command center displaying key business metrics, customer lifecycle trends, and quick access to critical functions -- **Customer Management** - Comprehensive customer database with import tools, status tracking, and detailed customer profiles for managing your entire customer base -- **Product Management** - Product catalog management with recommendation engine configuration, inventory tracking, and cross-selling relationship setup -- **Conversations** - Unified communication hub that manages all customer interactions including automated product recommendations, churn prevention surveys, and customer service requests across multiple channels -- **Task Management** - Automated business process monitoring and manual task execution for streamlined operations -- **Settings Center** - System-wide configuration including branding, integrations, communication templates, and team management -- **AI Assistant** - Intelligent business advisor providing data insights, trend analysis, and operational recommendations - -## Feature Modules - -### 1. Home Dashboard - -The Home Dashboard serves as your business command center, providing at-a-glance insights into customer health, business performance, and automated campaign management. This centralized view helps you make data-driven decisions and quickly identify areas requiring attention. - -**Main Features:** - -- **Customer Lifecycle Chart** - Visual representation of how customers move through different stages (Active → Potential → Churned), helping you identify trends in customer retention and acquisition patterns over time - -- **Customer Metrics Overview** - Real-time display of critical business KPIs including total customer count, active customer percentage, churn rate, and growth metrics to monitor business health - -- **Churn Survey Configuration** - Quick setup interface for automated retention campaigns, allowing you to configure when and how churn prevention surveys are sent to at-risk customers - -- **Business Task Processing** - Monitor and trigger automated business processes such as recommendation generation, email campaigns, and data synchronization tasks - -**Use Cases:** - -- **Business Health Monitoring**: Get instant visibility into customer retention trends and overall business performance -- **Trend Analysis**: Track customer lifecycle changes to identify seasonal patterns or business impact events -- **Campaign Management**: Configure and monitor automated marketing and retention campaigns from a central location -- **Operational Oversight**: Keep track of automated processes and ensure business operations are running smoothly - -### 2. Customer Management - -**Core Features:** - -#### 2.1 Customer List Management - -- **Customer Information Display**: Name, email, status, source, locale, creation date -- **Customer Status Categories**: - - `Active` - Active customers - - `Potential` - Potential customers - - `Churned` - Churned customers -- **Search and Filter**: Search by name/email, filter by status -- **Pagination**: Customizable page size (10/20/50 items) - -#### 2.2 Customer Import Functionality - -- **Manual Import**: Direct input of customer email lists - - Format: `customer@example.com` or `customer@example.com,locale` - - Supports batch import of multiple customers -- **File Import**: Supports Excel and CSV files - - Required fields: email - - Optional fields: locale (language setting) - - Supported formats: .xlsx, .xls, .csv - - Multi-language support: en, de, fr, zh, etc. and extended formats (en-US, de-DE, etc.) - -#### 2.3 Customer Operations - -- **View Subscriptions**: View customer product subscription details -- **Delete Customer**: Only manually imported customers can be deleted -- **Customer Details**: View complete customer profile information - -**Operation Guide:** - -1. Click "Import Churned" button to import churned customers -2. Select import method (manual input or file upload) -3. Input or upload customer data according to format requirements -4. System automatically validates and imports customer information - -### 3. Product Management - -**Main Features:** - -#### 3.1 Product Information Management - -- **Product Display**: Images, name, description, stock, creation date -- **Product Search**: Search by name or description -- **Product Details**: Expandable view for complete product information -- **External Links**: Direct navigation to product pages - -#### 3.2 Product Recommendation Settings - -- **Related Product Configuration**: Set recommendation relationships between products -- **Recommendation Metadata Management**: Configure recommendation algorithm parameters -- **Multi-language Support**: Multi-language translations for product names - -#### 3.3 Product Data Management - -- **Batch Import**: Import products from platforms like Shopify -- **Data Export**: Export product lists for analysis -- **Inventory Management**: Track product stock status -- **Product Types**: Distinguish between physical items and services - -**Operation Guide:** - -1. Use search box to quickly locate specific products -2. Click product name to expand detailed information -3. Use "Relationships" column to configure product recommendation associations -4. Access product pages through external link buttons - -### 4. Conversations Management - -The Conversations module serves as your central communication hub, managing all customer interactions across different channels and purposes. This system replaces traditional email management by providing a unified interface for automated marketing campaigns, customer service requests, and retention efforts. - -**What is the Conversations System?** -The Conversations feature consolidates all customer communications into organized threads, whether they're automated product recommendations sent to active customers, churn prevention surveys for at-risk customers, or service requests. Each conversation represents a complete communication thread with a specific customer, allowing you to track the entire interaction history and manage follow-ups effectively. - -**Core Features:** - -#### 4.1 Conversation List Management - -The conversation list provides a comprehensive view of all customer interactions with intelligent organization: - -- **Conversation Status Workflow**: - - `Pending` - New conversations awaiting staff review or customer response - - `Resolved` - Completed conversations where the customer's needs have been addressed - - `Spam` - Filtered unwanted or irrelevant messages - - `Archived` - Conversations automatically archived after 30 days of inactivity to keep the active list manageable - -#### 4.2 Conversation Categories and Organization - -The system automatically categorizes conversations based on their purpose and business context: - -- **Category Management**: - - - `Product Recommendation` - Automated recommendations sent to active customers based on their purchase history and preferences, designed to increase cross-selling and upselling opportunities - - `Service Request` - Customer-initiated inquiries about products, orders, support issues, or general questions requiring staff attention - - `Churn Survey` - Targeted retention campaigns sent to customers showing signs of disengagement, including feedback surveys and win-back offers - -- **Priority Settings**: Conversations are automatically assigned priority levels (High, Medium, Low) based on customer value, urgency indicators, and business rules to help staff focus on the most important interactions first - -- **Channel Management**: Supports multiple communication channels including Email, WhatsApp, and other messaging platforms, with plans for website chat integration - -#### 4.3 Message Management and Workflow - -Advanced message handling capabilities ensure efficient communication processing: - -- **Message Status Tracking**: - - - `Draft` - Messages being composed - - `Pending Approval` - Automated messages awaiting staff review before sending - - `Approved` - Messages cleared for delivery - - `Sent` - Successfully delivered messages - - `Failed` - Messages that couldn't be delivered - -- **Bulk Operations**: Streamline workflow with batch actions: - - - Bulk resolve multiple conversations simultaneously - - Mass send approved messages to multiple customers - - Batch update conversation status or priority - - Export conversation data for analysis - -- **Rich Media Support**: Handle various content types including images, videos, audio files, and documents to provide comprehensive customer support - -**Operation Guide:** - -1. **Filtering and Organization**: Use the filter system to view conversations by status (Pending, Resolved), category (Product Recommendation, Service Request, Churn Survey), priority level, or communication channel -2. **Conversation Details**: Click on any conversation to view the complete message thread, customer context, and interaction history -3. **Efficient Processing**: Utilize bulk operations to handle multiple similar conversations simultaneously, improving response times and operational efficiency -4. **Quick Search**: Use the search function to instantly locate specific conversations by customer name, email, keywords, or conversation content -5. **Approval Workflow**: Review and approve automated messages before they're sent to ensure quality and brand consistency - -### 5. Task Management - -**Features:** - -- **Task Status Tracking**: Pending, Resolved -- **Task Search**: Search tasks by keywords -- **Task Details**: View task execution details and results -- **Automated Processing**: System automatically executes business tasks - -**Usage Instructions:** - -- Monitor automated task execution status -- View task processing results and error information -- Manually trigger specific business processes - -### 6. Settings Center - -#### 6.1 General Settings - -- **Theme Settings**: - - Light Mode - Bright mode - - Dark Mode - Dark mode - - System - Follow system settings - -#### 6.2 Organization Settings - -- **Organization Information Management**: Name, logo, etc. -- **Website Settings**: Configure business website URL -- **Member Management**: Manage team member permissions - -#### 6.3 Channels Settings - -- **Email Settings**: Configure email sending parameters -- **Website Integration**: Website chatbot (in development) -- **API Integration**: API interface configuration (in development) -- **Teams Integration**: Team collaboration tools (in development) - -#### 6.4 Tone of Voice Settings - -- **Template Management**: - - Product recommendation templates - - Churn survey templates - - Potential customer recommendation templates -- **Tone Analysis**: Generate brand tone based on example emails -- **Multi-language Templates**: Support creating multi-language content - -#### 6.5 Integrations Settings - -- **Shopify Integration**: Connect Shopify store -- **Circuly Integration**: Connect Circuly platform -- **Third-party Services**: Other business system integrations - -**Configuration Guide:** - -1. Upload brand email examples in tone of voice settings -2. System automatically analyzes and generates brand tone description -3. Create and manage various message templates -4. Configure integration services to sync data - -### 7. AI Assistant (Ask AI) - -The AI Assistant is your intelligent business advisor, powered by advanced AI technology that understands your business data and can provide actionable insights, answer complex questions, and suggest optimization strategies. Think of it as having a data analyst and business consultant available 24/7. - -**What Can the AI Assistant Do?** -The AI Assistant has access to your complete business data including customer information, product catalog, conversation history, and performance metrics. It can analyze patterns, identify trends, and provide recommendations based on your specific business context. - -**Intelligent Features:** - -- **Business Q&A**: Ask natural language questions about your business data and receive detailed, contextual answers. Examples: "Which products have the highest churn rate?" or "What's our customer acquisition trend this quarter?" - -- **Conversation History**: Maintains a complete record of your AI interactions, allowing you to reference previous analyses and build upon earlier insights for deeper understanding - -- **Intelligent Analysis**: Performs complex data analysis and pattern recognition to identify business opportunities, potential issues, and optimization areas you might not have considered - -- **Operation Suggestions**: Provides AI-powered recommendations for improving business processes, customer engagement strategies, and operational efficiency based on your data patterns - -**Usage Tips and Examples:** - -- **Customer Analytics**: "Show me customer retention rates by product category" or "Which customers are most likely to churn next month?" -- **Business Performance**: "What's driving our recent sales increase?" or "Compare this quarter's performance to last quarter" -- **Marketing Optimization**: "Which product recommendations have the highest conversion rates?" or "What's the best time to send churn surveys?" -- **Operational Guidance**: "How can I improve our customer response times?" or "What automation opportunities exist in our workflow?" -- **Trend Analysis**: "What seasonal patterns do you see in our customer behavior?" or "How has our product mix changed over time?" - -## Permission Management - -The system supports role-based access control: - -- **Owner** - Organization owner with full permissions -- **Admin** - Administrator who can manage business settings -- **Developer** - Developer with access to technical settings -- **Member** - Regular member with basic operation permissions - -## Data Security - -- **Row Level Security**: Ensures users can only access their own business data -- **Permission Verification**: All operations undergo strict permission checks -- **Data Encryption**: Sensitive information is stored encrypted -- **Audit Logs**: Records audit trails of important operations - -## Frequently Asked Questions - -**Q: How to import customer data?** -A: Go to customer management page, click "Import Churned" button, select manual input or file upload method. - -**Q: How to set product recommendation relationships?** -A: In the product management page, click the "Relationships" column of the product row to configure. - -**Q: How to configure email templates?** -A: Go to Settings > Tone of Voice, select the corresponding template type to edit. - -**Q: What languages does the system support?** -A: Supports English, German, French, Chinese, and other languages. Check the support list when importing customers. - -## Technical Support - -For technical issues or feature consultation, please contact the technical support team. The system will be continuously updated and optimized, with usage guidance provided for new features upon release. diff --git a/docs/onedrive-background-sync.md b/docs/onedrive-background-sync.md deleted file mode 100644 index c43f2a9da6..0000000000 --- a/docs/onedrive-background-sync.md +++ /dev/null @@ -1,302 +0,0 @@ -# OneDrive Background Sync - -This document explains how the OneDrive background sync system works, including how member credentials are stored and used for scheduled syncs. - -## Overview - -When a user enables auto-sync for OneDrive files or folders, the system stores: - -1. The OneDrive item configuration (file/folder ID, path, etc.) -2. The **user ID** (Better Auth userId) of the member who enabled the sync -3. Sync settings and status - -When a background sync job runs (via cron or scheduled workflow), it: - -1. Queries active sync configurations -2. For each configuration, retrieves the Microsoft Graph access token for the **stored userId** -3. Uses that user's credentials to sync files from OneDrive - -## Schema Changes - -### onedriveSyncConfigs Table - -The `onedriveSyncConfigs` table now includes a `userId` field: - -```typescript -onedriveSyncConfigs: defineTable({ - organizationId: v.string(), // Better Auth organization ID - userId: v.string(), // Better Auth user ID (whose credentials to use for sync) - itemType: v.union(v.literal('file'), v.literal('folder')), - itemId: v.string(), // OneDrive item ID - itemName: v.string(), // File or folder name - itemPath: v.optional(v.string()), - targetBucket: v.string(), - storagePrefix: v.optional(v.string()), - status: v.union( - v.literal('active'), - v.literal('inactive'), - v.literal('error'), - ), - lastSyncAt: v.optional(v.number()), - lastSyncStatus: v.optional(v.string()), - errorMessage: v.optional(v.string()), - metadata: v.optional(v.any()), -}); -``` - -## How It Works - -### 1. User Enables Auto-Sync - -When a user enables auto-sync through the UI: - -```typescript -import { enableAutoSync } from '@/actions/onedrive/config/enable-auto-sync'; - -// User clicks "Enable Auto-Sync" on a folder -const result = await enableAutoSync(organizationId, { - itemType: 'folder', - folderId: 'folder-123', - folderName: 'Documents', - folderPath: '/Documents', - targetBucket: 'documents', -}); -``` - -The `enableAutoSync` action: - -1. Gets the current authenticated user -2. Creates a sync configuration with the user's ID -3. Stores it in the `onedriveSyncConfigs` table - -```typescript -// Inside enableAutoSync -const user = await getCurrentUser(); -if (!user) { - return { success: false, error: 'User not authenticated' }; -} - -await fetchMutation(api.documents.createOneDriveSyncConfig, { - organizationId: organizationId, - userId: user._id, // Store the user's ID - itemType: 'folder', - itemId: params.folderId, - itemName: params.folderName, - // ... other fields -}); -``` - -### 2. Background Sync Job Runs - -When a background sync job (cron or scheduled workflow) runs: - -```typescript -import { getMicrosoftGraphTokenForUser } from '@/lib/microsoft-graph-client-for-user'; -import { MicrosoftGraphClient } from '@/lib/microsoft-graph-client'; - -// Get all active sync configurations -const configs = await ctx.db - .query('onedriveSyncConfigs') - .withIndex('by_organizationId_and_status', (q) => - q.eq('organizationId', organizationId).eq('status', 'active'), - ) - .collect(); - -// Process each configuration -for (const config of configs) { - // Get Microsoft Graph token for the user who created this sync config - const token = await getMicrosoftGraphTokenForUser(config.userId); - - if (!token) { - console.error(`No token available for user ${config.userId}`); - continue; - } - - // Create Microsoft Graph client with the user's token - const graphClient = new MicrosoftGraphClient(token); - - // Sync files using the user's credentials - if (config.itemType === 'file') { - const fileContent = await graphClient.readFile(config.itemId); - // Upload to storage... - } else { - const files = await graphClient.listFiles({ folderId: config.itemId }); - // Process files... - } -} -``` - -### 3. Token Management - -The system automatically handles token refresh: - -```typescript -// getMicrosoftGraphTokenForUser checks if token is expired -export async function getMicrosoftGraphTokenForUser( - userId: string, -): Promise { - // Query Microsoft account for the specific user - const microsoftAccount = await fetchQuery( - api.accounts.getMicrosoftAccountByUserId, - { userId }, - ); - - // Check if token is expired - if (microsoftAccount.accessTokenExpiresAt < Date.now() + 5 * 60 * 1000) { - // Refresh the token if expired - const refreshed = await refreshMicrosoftTokenForUser( - microsoftAccount.refreshToken, - microsoftAccount.accountId, - ); - return refreshed.accessToken; - } - - return microsoftAccount.accessToken; -} -``` - -## API Reference - -### New Functions - -#### `getMicrosoftGraphTokenForUser(userId: string)` - -Retrieves the Microsoft Graph access token for a specific user (not the current authenticated user). - -**Location:** `services/platform/lib/microsoft-graph-client-for-user.ts` - -**Usage:** - -```typescript -import { getMicrosoftGraphTokenForUser } from '@/lib/microsoft-graph-client-for-user'; - -const token = await getMicrosoftGraphTokenForUser('user-123'); -if (token) { - // Use token to access Microsoft Graph API -} -``` - -#### `api.accounts.getMicrosoftAccountByUserId` - -Convex query to get Microsoft OAuth account for a specific user. - -**Usage:** - -```typescript -const account = await ctx.runQuery(api.accounts.getMicrosoftAccountByUserId, { - userId: 'user-123', -}); -``` - -### Updated Functions - -#### `createOneDriveSyncConfig` - -Now requires `userId` parameter: - -```typescript -await fetchMutation(api.documents.createOneDriveSyncConfig, { - organizationId: 'org-123', - userId: 'user-123', // NEW: Required field - itemType: 'file', - itemId: 'file-456', - itemName: 'document.pdf', - targetBucket: 'documents', -}); -``` - -#### `getOneDriveSyncConfigs` - -Now returns `userId` in the config objects: - -```typescript -const result = await fetchQuery(api.documents.getOneDriveSyncConfigs, { - organizationId: 'org-123', - status: 'active', -}); - -// result.configs[0].userId is now available -``` - -## Example: Background Sync Workflow - -Here's a complete example of a background sync workflow: - -```typescript -// services/platform/workflows/onedrive-sync.ts -import type { InlineWorkflowDefinition } from './types'; - -export const onedriveSyncWorkflow: InlineWorkflowDefinition = { - workflowConfig: { - name: 'OneDrive Auto Sync', - description: 'Sync files from OneDrive based on active sync configurations', - version: '1.0.0', - workflowType: 'predefined', - config: { - timeout: 300000, // 5 minutes - retryPolicy: { maxRetries: 3, backoffMs: 2000 }, - variables: { - organizationId: 'org_demo', - }, - }, - }, - stepsConfig: [ - { - stepSlug: 'start', - name: 'start', - stepType: 'trigger', - order: 1, - config: { - type: 'schedule', - schedule: '0 */1 * * *', // Every hour - timezone: 'UTC', - }, - nextSteps: { success: 'get_sync_configs' }, - }, - { - stepSlug: 'get_sync_configs', - name: 'Get Active Sync Configurations', - stepType: 'action', - order: 2, - config: { - type: 'custom', - // Query active sync configs and process each one - }, - nextSteps: { success: 'sync_files' }, - }, - // ... more steps - ], -}; -``` - -## Security Considerations - -1. **User Credentials**: Each sync configuration uses the credentials of the user who created it. If that user's Microsoft account is disconnected or their token expires and cannot be refreshed, the sync will fail. - -2. **Token Storage**: Microsoft Graph access tokens and refresh tokens are stored securely in the Better Auth accounts table. - -3. **Token Refresh**: The system automatically refreshes expired tokens before using them for sync operations. - -4. **Error Handling**: If a user's credentials are no longer valid, the sync configuration should be marked as 'error' status and the user should be notified to reconnect their Microsoft account. - -## Migration Notes - -If you have existing `onedriveSyncConfigs` records without a `userId` field, you'll need to: - -1. Run a migration to add the `userId` field to existing records -2. Either assign a default user or mark them as inactive until a user re-enables them - -Example migration: - -```typescript -// Mark all existing configs without userId as inactive -const configs = await ctx.db.query('onedriveSyncConfigs').collect(); -for (const config of configs) { - if (!config.userId) { - await ctx.db.patch(config._id, { - status: 'inactive', - errorMessage: 'Please re-enable sync to assign user credentials', - }); - } -} -``` diff --git a/docs/onedrive-debugging-guide.md b/docs/onedrive-debugging-guide.md deleted file mode 100644 index 78e976da1f..0000000000 --- a/docs/onedrive-debugging-guide.md +++ /dev/null @@ -1,203 +0,0 @@ -# OneDrive Integration Debugging Guide - -## Overview - -This guide explains how to debug "item not found" errors and other issues in the OneDrive integration. The debugging system provides detailed logging and error tracking to help identify root causes. - -## Potential Causes of "Item Not Found" Error - -### 1. File/Folder Deletion -- **Cause**: Items were deleted after being listed but before being accessed -- **Symptoms**: Error occurs when clicking on specific files/folders -- **Debug Info**: Check the debug logs for the specific file ID and name - -### 2. Permission Changes -- **Cause**: User permissions changed between listing and accessing files -- **Symptoms**: Intermittent access issues, especially in shared folders -- **Debug Info**: Look for 403 Forbidden errors in the logs - -### 3. Invalid File IDs -- **Cause**: File/folder IDs became invalid due to OneDrive sync issues -- **Symptoms**: Consistent errors for specific items -- **Debug Info**: Compare file IDs between different API calls - -### 4. Token Expiration -- **Cause**: Microsoft access token expired during the session -- **Symptoms**: Authentication errors, "session has expired" messages -- **Debug Info**: Check for token expiration errors in logs - -### 5. Race Conditions -- **Cause**: Multiple requests trying to access the same item simultaneously -- **Symptoms**: Sporadic errors, especially during rapid navigation -- **Debug Info**: Look for overlapping API calls in the logs - -### 6. OneDrive Sync Issues -- **Cause**: Items not properly synced between OneDrive and Graph API -- **Symptoms**: Files visible in OneDrive web but not accessible via API -- **Debug Info**: Check for 404 errors with valid-looking file IDs - -## Debug Panel Features - -### Development Mode Only -The debug panel only appears when `NODE_ENV=development` to avoid cluttering the production interface. - -### Real-time Logging -- **API Calls**: All Microsoft Graph API requests are logged with timing -- **Errors**: Detailed error information including status codes and messages -- **User Actions**: File clicks, folder navigation, and search operations -- **Authentication**: Token status and refresh attempts - -### Debug Information Displayed -- Current folder ID -- Authentication status -- Total number of logs -- Error count -- Recent errors with expandable details -- Complete log history (last 20 entries) - -### Interactive Features -- **Expand/Collapse**: Click on log entries to see detailed information -- **Copy to Clipboard**: Copy error details for sharing or analysis -- **Clear Logs**: Reset the debug log history -- **Refresh Auth**: Force authentication refresh - -## Enhanced Error Handling - -### Microsoft Graph Service Improvements -- **Detailed Error Logging**: Captures HTTP status codes, error codes, and messages -- **Performance Tracking**: Measures API call duration -- **Enhanced Error Objects**: Includes additional context for debugging - -### Component-Level Debugging -- **Operation Tracking**: Logs all user interactions and their outcomes -- **Error Context**: Provides file names, IDs, and operation details -- **Timing Information**: Tracks how long operations take - -## How to Use the Debug Panel - -### 1. Enable Development Mode -```bash -# Make sure you're running in development mode -NODE_ENV=development npm run dev -``` - -### 2. Access the Debug Panel -- Navigate to the OneDrive demo page -- The debug panel will appear at the top of the page -- Click the chevron icon to expand/collapse the panel - -### 3. Reproduce the Issue -- Perform the actions that trigger the "item not found" error -- Watch the debug logs populate in real-time -- Note any error entries that appear - -### 4. Analyze the Logs -- Look for error entries (red background) -- Click on error entries to expand details -- Check the timing of operations -- Look for patterns in failed requests - -### 5. Common Debug Scenarios - -#### Scenario 1: File Access Error -```json -{ - "type": "error", - "operation": "Failed to read file content", - "details": { - "fileId": "01BYE5RZ...", - "fileName": "document.txt", - "error": "Microsoft Graph API error: Item not found" - } -} -``` -**Action**: Check if the file still exists in OneDrive web interface - -#### Scenario 2: Authentication Error -```json -{ - "type": "error", - "operation": "Failed to load files", - "details": { - "error": "Your Microsoft session has expired. Please sign in again." - } -} -``` -**Action**: Use the "Refresh Auth" button or reload the page - -#### Scenario 3: Permission Error -```json -{ - "type": "error", - "operation": "Failed to load files", - "details": { - "error": "Microsoft Graph API error: Access denied", - "folderId": "01BYE5RZ..." - } -} -``` -**Action**: Check folder permissions in OneDrive - -## Troubleshooting Steps - -### Step 1: Check Authentication -1. Look for authentication errors in the debug panel -2. Verify the user is signed in with Microsoft -3. Try refreshing authentication using the debug panel button - -### Step 2: Verify File/Folder Existence -1. Note the file ID from the error logs -2. Check if the file exists in OneDrive web interface -3. Try accessing the file directly in OneDrive - -### Step 3: Check Permissions -1. Verify the user has access to the folder/file -2. Check if it's a shared folder with restricted access -3. Try accessing as the file owner - -### Step 4: Monitor API Calls -1. Watch the debug logs for API call patterns -2. Look for failed requests and their error codes -3. Check if errors are consistent or intermittent - -### Step 5: Test Different Scenarios -1. Try different files and folders -2. Test with different user accounts -3. Compare behavior between file types - -## Production Considerations - -### Logging in Production -- Debug panel is automatically hidden in production -- Server-side logging still captures errors -- Use application monitoring tools for production debugging - -### Error Handling -- Users see friendly error messages -- Detailed errors are logged server-side -- Automatic retry mechanisms for transient errors - -### Performance -- Debug logging has minimal performance impact -- Logs are limited to prevent memory issues -- Only essential information is captured in production - -## Getting Help - -If you continue to experience "item not found" errors: - -1. **Collect Debug Information**: - - Copy error details from the debug panel - - Note the specific files/folders affected - - Record the sequence of actions that trigger the error - -2. **Check Microsoft Graph API Status**: - - Visit the Microsoft 365 Service Health Dashboard - - Look for known issues with OneDrive or Graph API - -3. **Contact Support**: - - Provide the debug information collected - - Include screenshots of the debug panel - - Describe the user's OneDrive setup and permissions - -This debugging system should help identify the root cause of "item not found" errors and provide the information needed to resolve them effectively. diff --git a/docs/onedrive-integration-guide.md b/docs/onedrive-integration-guide.md deleted file mode 100644 index ab9c5fe662..0000000000 --- a/docs/onedrive-integration-guide.md +++ /dev/null @@ -1,565 +0,0 @@ -# OneDrive Integration Guide - -This guide explains how to use OneDrive file synchronization features in the frontend, including manual upload and automatic sync functionality. - -## Overview - -Our OneDrive integration provides two distinct sync methods: - -1. **Manual Upload (One-time)** - Direct file selection and immediate sync without persistent configuration -2. **Auto Sync** - Persistent sync configurations that can be triggered manually or run automatically - -## 🔄 Manual Upload (One-time Sync) - -### What is Manual Upload? - -Manual upload allows users to browse OneDrive files, select specific files/folders, and sync them immediately to Supabase storage without creating any persistent auto-sync configuration. - -### Frontend Implementation - -#### 1. Using OneDrive File Browser Component - -```tsx -import OneDriveFileBrowser from '@/components/onedrive/onedrive-file-browser'; - -function DocumentsPage({ businessId }: { businessId: string }) { - return ( -
-

Documents

- -
- ); -} -``` - -#### 2. Using OneDrive Import Dialog - -```tsx -import OneDriveImportDialog from '@/app/(app)/dashboard/[id]/documents/components/onedrive-import-dialog'; - -function ImportButton({ businessId }: { businessId: string }) { - const [isOpen, setIsOpen] = useState(false); - - const handleSuccess = () => { - // Handle successful import - setIsOpen(false); - // Refresh your document list - }; - - return ( - - ); -} -``` - -#### 3. Direct API Call (Advanced) - -```tsx -async function syncSelectedFiles( - businessId: string, - selectedFiles: Array<{ - id: string; - name: string; - size?: number; - relativePath?: string; - }>, -) { - const response = await fetch('/api/documents/onedrive', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - items: selectedFiles, - businessId, - importType: 'one-time', // Important: Use 'one-time' for manual upload - }), - }); - - if (!response.ok) { - throw new Error('Sync failed'); - } - - // Handle streaming response - const reader = response.body?.getReader(); - // Process SSE events... -} -``` - -### Key Features - -- ✅ No persistent configuration required -- ✅ Immediate file sync -- ✅ User selects files directly from OneDrive browser -- ✅ Real-time progress updates via SSE -- ✅ Support for both files and folders - -## ⚙️ Auto Sync Configuration - -### What is Auto Sync? - -Auto sync creates persistent configurations that can sync OneDrive files/folders automatically on a schedule or be triggered manually. These configurations are stored in the `OneDriveAutoSync` table. - -### Frontend Implementation - -#### 1. Using Auto Sync Manager Component - -```tsx -import AutoSyncManager from '@/components/onedrive/auto-sync-manager'; - -function OneDriveSettingsPage({ businessId }: { businessId: string }) { - return ( -
-

OneDrive Auto Sync

- -
- ); -} -``` - -#### 2. Enable Auto Sync for Specific Items - -```tsx -import { enableAutoSync } from '@/actions/onedrive/config/enable-auto-sync'; - -async function enableFolderAutoSync(businessId: string, folderId: string) { - const result = await enableAutoSync(businessId, { - itemType: 'folder', - folderId: folderId, - folderName: 'My Documents', - folderPath: '/Documents', - targetBucket: 'documents', - syncFrequencyMinutes: 60, // Sync every hour - maxFileSizeMb: 100, - allowedExtensions: ['pdf', 'docx', 'xlsx'], - excludePatterns: ['*.tmp', 'temp/*'], - }); - - if (result.success) { - console.log('Auto sync enabled:', result.data); - } else { - console.error('Failed to enable auto sync:', result.error); - } -} -``` - -#### 3. Manually Trigger Auto Sync - -```tsx -import { runAutoSyncOnce } from '@/actions/onedrive/sync/run-auto-sync-once'; - -async function triggerSync(businessId: string, itemId: string) { - const result = await runAutoSyncOnce(businessId, itemId); - - if (result.success) { - console.log(`Synced ${result.successfulFiles}/${result.totalFiles} files`); - } else { - console.error('Sync failed:', result.error); - } -} -``` - -### Auto Sync Configuration Options - -```typescript -interface AutoSyncConfig { - itemType: 'file' | 'folder'; - - // For files - fileId?: string; - fileName?: string; - filePath?: string; - - // For folders - folderId?: string; - folderName?: string; - folderPath?: string; - - // Common options - targetBucket?: string; // Default: 'documents' - targetPath?: string; - storagePrefix?: string; - syncFrequencyMinutes?: number; // Default: 60 - maxFileSizeMb?: number; // Default: 100 - allowedExtensions?: string[]; - blockedExtensions?: string[]; - includePatterns?: string[]; - excludePatterns?: string[]; -} -``` - -## 📊 Sync Status and Monitoring - -### Check Sync Status - -```tsx -import { getOneDriveSyncStatus } from '@/actions/onedrive/sync-status'; - -async function checkSyncStatus() { - const result = await getOneDriveSyncStatus('documents'); - - if (result.success) { - result.syncedFiles.forEach((fileInfo, oneDriveId) => { - console.log(`File ${fileInfo.fileName} synced at ${fileInfo.lastSynced}`); - }); - } -} -``` - -### Get Sync Statistics - -```tsx -import { getSyncStatistics } from '@/actions/onedrive/sync-status'; - -async function loadSyncStats() { - const stats = await getSyncStatistics('documents'); - console.log(`Total synced files: ${stats.totalFiles}`); - console.log(`Total size: ${stats.totalSizeMB} MB`); -} -``` - -## 🔧 Available Actions and Routes - -### Server Actions - -| Action | File | Purpose | -| ----------------------- | ----------------------------------------------------------- | ------------------------------ | -| `enableAutoSync` | `/actions/onedrive/config/enable-auto-sync.ts` | Create/update auto-sync config | -| `disableAutoSync` | `/actions/onedrive/config/disable-auto-sync.ts` | Disable auto-sync config | -| `runAutoSyncOnce` | `/actions/onedrive/sync/run-auto-sync-once.ts` | Manually trigger auto-sync | -| `getOneDriveSyncStatus` | `/actions/onedrive/sync-status/get-onedrive-sync-status.ts` | Check file sync status | -| `listOneDriveFiles` | `/actions/onedrive/list-files.ts` | Browse OneDrive files | - -### API Routes - -| Route | Method | Purpose | -| ------------------------- | -------- | -------------------------- | -| `/api/documents/onedrive` | POST | Manual file upload/sync | -| `/api/cron/onedrive-sync` | GET/POST | Scheduled auto-sync (cron) | - -## 🎯 Usage Examples - -### Example 1: Simple File Browser - -```tsx -'use client'; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { listOneDriveFiles } from '@/actions/onedrive/list-files'; - -export default function SimpleFileBrowser() { - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(false); - - const loadFiles = async () => { - setLoading(true); - try { - const result = await listOneDriveFiles({ folderId: 'root' }); - if (result.success) { - setFiles(result.files); - } - } finally { - setLoading(false); - } - }; - - return ( -
- - -
- {files.map((file) => ( -
- {file.name} -
- ))} -
-
- ); -} -``` - -### Example 2: Auto Sync Management - -```tsx -'use client'; - -import { useState, useEffect } from 'react'; -import { Button } from '@/components/ui/button'; -import { enableAutoSync, getAutoSyncConfigs } from '@/actions/onedrive/config'; -import { runAutoSyncOnce } from '@/actions/onedrive/sync'; - -export default function AutoSyncManager({ - businessId, -}: { - businessId: string; -}) { - const [configs, setConfigs] = useState([]); - const [loading, setLoading] = useState(false); - - const loadConfigs = async () => { - const result = await getAutoSyncConfigs(businessId); - if (result.success) { - setConfigs(result.configs); - } - }; - - const handleEnableAutoSync = async (folderId: string, folderName: string) => { - setLoading(true); - try { - const result = await enableAutoSync(businessId, { - itemType: 'folder', - folderId, - folderName, - folderPath: `/${folderName}`, - syncFrequencyMinutes: 60, - }); - - if (result.success) { - await loadConfigs(); // Refresh list - } - } finally { - setLoading(false); - } - }; - - const handleManualSync = async (configId: string) => { - const result = await runAutoSyncOnce(businessId, configId); - if (result.success) { - console.log('Sync completed successfully'); - } - }; - - useEffect(() => { - loadConfigs(); - }, [businessId]); - - return ( -
-

Auto Sync Configurations

- - {configs.map((config) => ( -
-

{config.folder_name || config.file_name}

-

Status: {config.last_sync_status}

-

Last sync: {config.last_sync_at}

- - -
- ))} -
- ); -} -``` - -### Example 3: Streaming File Upload with Progress - -```tsx -'use client'; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Progress } from '@/components/ui/progress'; - -export default function StreamingUpload({ - businessId, -}: { - businessId: string; -}) { - const [progress, setProgress] = useState(0); - const [currentFile, setCurrentFile] = useState(''); - const [isUploading, setIsUploading] = useState(false); - - const handleStreamingUpload = async (selectedFiles: any[]) => { - setIsUploading(true); - setProgress(0); - - try { - const response = await fetch('/api/documents/onedrive', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - items: selectedFiles, - businessId, - importType: 'one-time', - }), - }); - - if (!response.body) { - throw new Error('No response body'); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - const lines = chunk.split('\n'); - - for (const line of lines) { - if (line.startsWith('data: ')) { - try { - const data = JSON.parse(line.slice(6)); - - if (data.progress !== undefined) { - setProgress(data.progress); - setCurrentFile(data.currentFile || ''); - } - - if (data.success !== undefined) { - // Upload completed - setProgress(100); - setCurrentFile(''); - } - } catch (e) { - console.error('Failed to parse SSE data:', e); - } - } - } - } - } catch (error) { - console.error('Upload failed:', error); - } finally { - setIsUploading(false); - } - }; - - return ( -
- {isUploading && ( -
-
- Uploading: {currentFile} -
- -
- {progress.toFixed(1)}% complete -
-
- )} - - -
- ); -} -``` - -## 🚨 Important Notes - -### Authentication Requirements - -All OneDrive operations require valid Microsoft authentication. Make sure users are properly authenticated before calling any OneDrive actions. - -```tsx -// Check authentication status -import { getCurrentUser } from '@/lib/auth/auth-server'; - -const user = await getCurrentUser(); -if (!user) { - // Redirect to login or show auth error - return; -} -``` - -### Error Handling - -Always implement proper error handling for OneDrive operations: - -```tsx -try { - const result = await someOneDriveAction(); - if (!result.success) { - // Handle specific error - if (result.error === 'Microsoft authentication required') { - // Redirect to Microsoft auth - } else { - // Show generic error message - } - } -} catch (error) { - console.error('OneDrive operation failed:', error); - // Show user-friendly error message -} -``` - -### Performance Considerations - -1. **Pagination**: Use pagination for large file lists -2. **Debouncing**: Debounce search inputs when browsing files -3. **Caching**: Cache file lists when appropriate -4. **Progress Feedback**: Always show progress for long-running operations - -### Security Notes - -1. **Business ID Validation**: Always validate businessId on the server side -2. **User Permissions**: Ensure users can only access their own business data -3. **File Size Limits**: Respect configured file size limits -4. **File Type Restrictions**: Honor allowed/blocked file extensions - -## 🔍 Troubleshooting - -### Common Issues - -1. **"Auto-sync configuration not found"** - - - Make sure to create auto-sync config before triggering manual sync - - Use `enableAutoSync` first, then `runAutoSyncOnce` - -2. **"Microsoft authentication required"** - - - User needs to reconnect their Microsoft account - - Redirect to Microsoft OAuth flow - -3. **Files not syncing** - - - Check file size limits - - Verify file extensions are allowed - - Check exclude patterns - -4. **Slow sync performance** - - Reduce batch size - - Check network connectivity - - Monitor server resources - -### Debug Information - -Enable debug logging to troubleshoot issues: - -```tsx -// Add debug logging to your components -console.log('OneDrive operation:', { - businessId, - operation: 'sync', - timestamp: new Date().toISOString(), -}); -``` - -## 📚 Related Documentation - -- [Microsoft Graph API Documentation](https://docs.microsoft.com/en-us/graph/) -- [Next.js Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) -- [Supabase Storage Documentation](https://supabase.com/docs/guides/storage) - ---- - -For additional help or questions, please contact the development team or check the codebase for implementation details. diff --git a/docs/onedrive-parent-directory-tracking.md b/docs/onedrive-parent-directory-tracking.md deleted file mode 100644 index 2a3a64a0c4..0000000000 --- a/docs/onedrive-parent-directory-tracking.md +++ /dev/null @@ -1,106 +0,0 @@ -# OneDrive Parent Directory Tracking - -## Overview - -This feature enhances the OneDrive sync functionality to track whether files were directly selected by the user or synchronized from a parent directory. When files are synced from a folder, the system now records which parent directory was originally selected by the user. - -## New Metadata Fields - -When files are synced to Supabase storage, the following new metadata fields are now included: - -### Core Selection Tracking -- `isDirectlySelected` (boolean): Whether the file was directly selected by the user -- `syncType` (string): Either "direct" or "folder" (existing field, enhanced) - -### Parent Directory Information -- `selectedParentId` (string): OneDrive ID of the parent directory that was directly selected -- `selectedParentName` (string): Name of the parent directory that was directly selected -- `selectedParentPath` (string): Path of the parent directory that was directly selected - -## Use Cases - -### Scenario 1: Direct File Selection -When a user directly selects individual files: -```json -{ - "isDirectlySelected": true, - "syncType": "direct", - "selectedParentId": null, - "selectedParentName": null, - "selectedParentPath": null -} -``` - -### Scenario 2: Folder Selection -When a user selects a folder named "Documents" containing subfolders and files: -```json -{ - "isDirectlySelected": false, - "syncType": "folder", - "selectedParentId": "folder-abc123", - "selectedParentName": "Documents", - "selectedParentPath": "Documents" -} -``` - -### Scenario 3: Nested Folder Structure -For a file at `Documents/Projects/2024/report.pdf` where "Documents" was selected: -```json -{ - "isDirectlySelected": false, - "syncType": "folder", - "selectedParentId": "folder-abc123", - "selectedParentName": "Documents", - "selectedParentPath": "Documents", - "relativePath": "Documents/Projects/2024" -} -``` - -## Benefits - -1. **Traceability**: Users can identify which parent directory they originally selected -2. **Organization**: Better understanding of file origins in storage -3. **Debugging**: Easier troubleshooting of sync operations -4. **User Experience**: Clear indication of how files were synchronized - -## Implementation Details - -### Frontend Changes -- Enhanced `collectAllFiles` function to track selected parent information -- Updated sync handler to pass selection context -- Modified file processing to include parent directory metadata - -### Backend Changes -- Updated API route to accept and process parent directory information -- Enhanced storage metadata with new fields -- Updated type definitions across the codebase - -### Database Schema -The metadata is stored in Supabase storage's `user_metadata` field as JSON: -```sql --- Example storage object metadata -{ - "oneDriveId": "file-123", - "oneDriveName": "document.pdf", - "source": "onedrive", - "syncType": "folder", - "isDirectlySelected": false, - "selectedParentId": "folder-abc123", - "selectedParentName": "Documents", - "selectedParentPath": "Documents", - "syncedAt": "2024-01-15T12:00:00Z" -} -``` - -## Backward Compatibility - -- Existing synced files without the new metadata fields will continue to work -- New fields are optional and won't break existing functionality -- Legacy files can be identified by the absence of `isDirectlySelected` field - -## Future Enhancements - -- UI indicators showing file selection source -- Filtering capabilities based on selection type -- Bulk operations on files from specific parent directories -- Analytics on user selection patterns diff --git a/docs/row-level-security-guide.md b/docs/row-level-security-guide.md deleted file mode 100644 index 9e2632ae70..0000000000 --- a/docs/row-level-security-guide.md +++ /dev/null @@ -1,651 +0,0 @@ -# Row Level Security (RLS) Implementation Guide - -This guide provides a complete solution for implementing Row Level Security in your Convex application using the [convex-helpers](https://stack.convex.dev/row-level-security) approach, ensuring users can only access data from their organizations. - -## Table of Contents - -1. [Quick Start](#quick-start) -2. [Core Library](#core-library) -3. [Implementation Patterns](#implementation-patterns) -4. [Migration Guide](#migration-guide) -5. [Best Practices](#best-practices) -6. [Troubleshooting](#troubleshooting) - -## Quick Start - -**The only file you need**: `convex/lib/rls-helpers.ts` provides everything for secure RLS using convex-helpers. - -```typescript -// Simple, secure RLS pattern - authorization happens automatically! -import { queryWithRLS, mutationWithRLS } from './lib/rls-helpers'; - -export const listDocuments = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - returns: v.array(v.any()), - handler: async (ctx, args) => { - // RLS automatically validates access and filters results - return await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - }, -}); -``` - -**Key Benefits:** - -- **🔒 Security**: Automatic organization isolation at the database layer -- **⚡ Performance**: No performance overhead - rules check only accessed documents -- **🛠 Type Safety**: Full TypeScript support with proper error handling -- **📈 Scalable**: Authorization checked per-document as needed -- **🎯 Simple**: No manual validation needed - just use `queryWithRLS`/`mutationWithRLS` - -## Core Library - -**File**: `convex/lib/rls-helpers.ts` - This wraps your database with automatic RLS enforcement. - -### Essential Functions - -```typescript -// 🔑 Core wrappers - use these instead of query/mutation -import { queryWithRLS, mutationWithRLS } from './lib/rls-helpers'; - -// All database operations automatically check RLS rules -export const myQuery = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - handler: async (ctx, args) => { - // ctx.db is wrapped - all queries automatically filtered - return await ctx.db.query('documents').collect(); - }, -}); - -// 🛠 Helper functions (optional, for custom logic) -import { getAuthUser, getUserOrganizations } from './lib/rls-helpers'; - -const user = await getAuthUser(ctx); // Get current user (returns null if not authenticated) -const orgs = await getUserOrganizations(ctx); // Get user's organizations -``` - -### How It Works - -The RLS system uses `convex-helpers` to wrap the database context: - -1. **Custom Wrappers**: `queryWithRLS` and `mutationWithRLS` wrap the standard Convex functions -2. **Database Interception**: `ctx.db` is wrapped to intercept all database operations -3. **Automatic Filtering**: Every `db.get()`, `db.query()`, `db.patch()`, `db.delete()`, and `db.insert()` is checked against RLS rules -4. **Centralized Rules**: All rules defined in one place (`rls-helpers.ts`) - -### Multi-Tenant Data Model - -```typescript -// Every business entity includes organizationId -interface BusinessEntity { - organizationId: Id<'organizations'>; - // ... other fields -} - -// Users belong to organizations through membership -interface Member { - organizationId: Id<'organizations'>; - identityId: string; // Links to auth user - role: 'Owner' | 'Admin' | 'Developer' | 'Member'; -} -``` - -### Security Flow - -1. **Authentication**: Verify user identity using Better Auth -2. **Organization Membership**: Load user's organizations and roles -3. **Database Wrapping**: Wrap `ctx.db` with RLS enforcement -4. **Automatic Filtering**: Each database operation checks access rules - -### RLS Rules - -Rules are defined in `convex/lib/rls-helpers.ts`: - -```typescript -const rules = { - documents: { - read: async (rlsCtx, document) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(document.organizationId); - }, - modify: async (rlsCtx, document) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(document.organizationId); - }, - insert: async (rlsCtx, document) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(document.organizationId); - }, - }, - // ... rules for other tables -}; -``` - -## Implementation Patterns - -### Pattern 1: Simple Organization Query - -```typescript -import { queryWithRLS } from './lib/rls-helpers'; - -export const listDocuments = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - returns: v.array(v.any()), - handler: async (ctx, args) => { - // RLS automatically filters to only documents user can access - return await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .order('desc') - .take(20); - }, -}); -``` - -### Pattern 2: Resource Update (Automatic Validation) - -```typescript -import { mutationWithRLS } from './lib/rls-helpers'; - -export const updateDocument = mutationWithRLS({ - args: { - documentId: v.id('documents'), - title: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - // RLS automatically validates user has access before allowing the patch - const document = await ctx.db.get(args.documentId); - if (!document) { - throw new Error('Document not found'); - } - - // This will throw if user doesn't have modify permission - await ctx.db.patch(args.documentId, { - title: args.title, - }); - - return null; - }, -}); -``` - -### Pattern 3: Cross-Resource Query - -```typescript -import { queryWithRLS } from './lib/rls-helpers'; - -export const getOrganizationStats = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - returns: v.object({ - totalCustomers: v.number(), - totalProducts: v.number(), - totalDocuments: v.number(), - }), - handler: async (ctx, args) => { - // All queries automatically filtered to user's accessible data - const [customers, products, documents] = await Promise.all([ - ctx.db - .query('customers') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(), - ctx.db - .query('products') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(), - ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(), - ]); - - return { - totalCustomers: customers.length, - totalProducts: products.length, - totalDocuments: documents.length, - }; - }, -}); -``` - -### Pattern 4: Unauthenticated Queries - -```typescript -import { queryWithRLS, getAuthUser } from './lib/rls-helpers'; - -export const listPublicDocuments = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - returns: v.array(v.any()), - handler: async (ctx, args) => { - // Check if user is authenticated - const user = await getAuthUser(ctx); - if (!user) { - // Return empty array for unauthenticated users - return []; - } - - // RLS automatically filters - return await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - }, -}); -``` - -## Migration Guide - -### Step 1: Update Imports - -**Before** (Old manual validation): - -```typescript -import { query, mutation } from './_generated/server'; -import { validateOrganizationAccess } from './lib/rls'; -``` - -**After** (New automatic RLS): - -```typescript -import { queryWithRLS, mutationWithRLS } from './lib/rls-helpers'; -``` - -### Step 2: Replace Function Wrappers - -**Before** (Manual validation): - -```typescript -export const getDocuments = query({ - args: { organizationId: v.id('organizations') }, - returns: v.array(v.any()), - handler: async (ctx, args) => { - // Manual validation required - await validateOrganizationAccess(ctx, args.organizationId); - - return await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - }, -}); -``` - -**After** (Automatic RLS): - -```typescript -export const getDocuments = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - returns: v.array(v.any()), - handler: async (ctx, args) => { - // No manual validation needed - RLS handles it automatically! - return await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - }, -}); -``` - -### Step 3: Remove Manual Auth Checks - -**Before** (Manual checks): - -```typescript -export const createDocument = mutation({ - args: { organizationId: v.id('organizations'), title: v.string() }, - handler: async (ctx, args) => { - // Manual authentication check - const authUser = await authComponent.getAuthUser(ctx); - if (!authUser) { - throw new Error('Not authenticated'); - } - - // Manual membership check - const member = await ctx.db - .query('members') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .filter((q) => q.eq(q.field('identityId'), authUser.userId)) - .first(); - - if (!member) { - throw new Error('Not authorized'); - } - - return await ctx.db.insert('documents', { - organizationId: args.organizationId, - title: args.title, - }); - }, -}); -``` - -**After** (Automatic RLS): - -```typescript -export const createDocument = mutationWithRLS({ - args: { organizationId: v.id('organizations'), title: v.string() }, - returns: v.id('documents'), - handler: async (ctx, args) => { - // RLS automatically validates authentication and authorization - return await ctx.db.insert('documents', { - organizationId: args.organizationId, - title: args.title, - }); - }, -}); -``` - -### Step 4: Test Your Migration - -```typescript -// Test: User cannot access other organization's data -test('RLS blocks cross-organization access', async () => { - try { - await testQuery(api.documents.getDocuments, { - organizationId: otherOrganizationId, - }); - // Should return empty array or throw error - } catch (error) { - // RLS automatically prevents access - expect(error).toBeDefined(); - } -}); -``` - -## Best Practices - -### 1. Always Use RLS Wrappers - -```typescript -// ❌ Bad: Manual validation prone to errors -import { query } from './_generated/server'; - -export const badQuery = query({ - handler: async (ctx, args) => { - await validateOrganizationAccess(ctx, args.organizationId); - return await ctx.db.query('documents').collect(); - }, -}); - -// ✅ Good: Automatic RLS enforcement -import { queryWithRLS } from './lib/rls-helpers'; - -export const goodQuery = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - handler: async (ctx, args) => { - // RLS automatically validates and filters - return await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - }, -}); -``` - -### 2. Centralize RLS Rules - -All authorization rules are defined in `convex/lib/rls-helpers.ts`. To add new table rules: - -```typescript -const rules = { - // Add your new table here - myNewTable: { - read: async (rlsCtx, row) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(row.organizationId); - }, - modify: async (rlsCtx, row) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(row.organizationId); - }, - insert: async (rlsCtx, row) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(row.organizationId); - }, - }, -}; -``` - -### 3. Role-Based Rules - -Different tables can have different role requirements: - -```typescript -// In rls-helpers.ts -integrations: { - read: async (rlsCtx, integration) => { - if (!rlsCtx.user) return false; - return userOrgIds.has(integration.organizationId); - }, - modify: async (rlsCtx, integration) => { - if (!rlsCtx.user) return false; - if (!userOrgIds.has(integration.organizationId)) return false; - - // Require Developer, Admin, or Owner role for integrations - const membership = userOrganizations.find( - (m) => m.organizationId === integration.organizationId, - ); - return ( - membership?.role === 'Owner' || - membership?.role === 'Admin' || - membership?.role === 'Developer' - ); - }, -}, -``` - -**Role Hierarchy** (higher roles inherit lower permissions): - -- **Member**: Regular member with basic operation permissions -- **Developer**: Developer with access to technical settings -- **Admin**: Administrator who can manage business settings -- **Owner**: Organization owner with full permissions - -### 4. Optimize Queries with Proper Indexing - -```typescript -// Ensure all organization-scoped tables have proper indexes -documents: defineTable({ - organizationId: v.id('organizations'), - // ... other fields -}) - .index('by_organizationId', ['organizationId']) - .index('by_organizationId_and_kind', ['organizationId', 'kind']); -``` - -### 5. Handle Errors Gracefully - -```typescript -import { queryWithRLS } from './lib/rls-helpers'; - -export const getDocument = queryWithRLS({ - args: { documentId: v.id('documents') }, - returns: v.union(v.any(), v.null()), - handler: async (ctx, args) => { - // RLS will return null if user doesn't have access - const document = await ctx.db.get(args.documentId); - - if (!document) { - // Could be not found OR no access - both return null - return null; - } - - return document; - }, -}); -``` - -## Troubleshooting - -### Performance Issues - -**Problem**: "Function execution timed out" - -**Solution**: RLS rules are efficient because they only check documents that are actually accessed. However, ensure you're using indexes: - -```typescript -// ❌ Bad: Full table scan -const documents = await ctx.db.query('documents').collect(); // Checks EVERY document against RLS rules - -// ✅ Good: Indexed query -const documents = await ctx.db - .query('documents') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); // Only checks relevant documents -``` - -### Authorization Errors - -**Problem**: "User cannot access data they should have access to" - -**Debug**: Check RLS rules in `rls-helpers.ts` - -```typescript -// Add logging to debug RLS rules -read: async (rlsCtx, document) => { - console.log('Checking read access:', { - user: rlsCtx.user?.userId, - documentOrg: document.organizationId, - userOrgs: rlsCtx.userOrganizations?.map(o => o.organizationId), - }); - - if (!rlsCtx.user) return false; - return userOrgIds.has(document.organizationId); -}, -``` - -**Solution**: Ensure user is a member of the organization with correct role. - -### Database Schema - -**Required**: All business tables must have organization indexes - -```typescript -// Add to convex/schema.ts -documents: defineTable({ - organizationId: v.id('organizations'), - // ... other fields -}).index('by_organizationId', ['organizationId']), - -products: defineTable({ - organizationId: v.id('organizations'), - // ... other fields -}).index('by_organizationId', ['organizationId']), -``` - -### RLS Not Working - -**Problem**: "RLS rules not being enforced" - -**Checklist**: - -1. ✅ Using `queryWithRLS`/`mutationWithRLS` instead of `query`/`mutation` -2. ✅ Table has rules defined in `rls-helpers.ts` -3. ✅ User is authenticated (check `getAuthUser()`) -4. ✅ User is member of organization (check membership table) - -## Architecture - -### File Structure - -``` -convex/ -├── lib/ -│ ├── rls-helpers.ts # New: convex-helpers based RLS (USE THIS) -│ └── rls.ts # Old: manual validation (deprecated) -├── chat.ts # Uses queryWithRLS/mutationWithRLS -├── conversations.ts # Uses queryWithRLS/mutationWithRLS -├── customers.ts # Uses queryWithRLS/mutationWithRLS -└── ... -``` - -### Key Differences from Old Approach - -| Aspect | Old Approach (`rls.ts`) | New Approach (`rls-helpers.ts`) | -| ------------------ | ------------------------------------- | ------------------------------- | -| **Validation** | Manual `validateOrganizationAccess()` | Automatic via database wrapper | -| **Location** | Check at function start | Check at database operation | -| **Coverage** | Easy to forget to validate | Impossible to bypass | -| **Performance** | Validates upfront | Only validates accessed data | -| **Complexity** | Need to remember to call | Just use `queryWithRLS` | -| **Error Handling** | Manual throw statements | Automatic permission errors | - -## Summary - -**What you need**: - -- ✅ `convex/lib/rls-helpers.ts` - Automatic RLS enforcement -- ✅ Organization indexes on all business tables -- ✅ Use `queryWithRLS` instead of `query` -- ✅ Use `mutationWithRLS` instead of `mutation` - -**What you DON'T need**: - -- ❌ Manual `validateOrganizationAccess()` calls -- ❌ Manual auth checks with `authComponent.getAuthUser()` -- ❌ Manual membership queries -- ❌ Complex wrapper functions - -**Simple pattern**: - -```typescript -import { queryWithRLS, mutationWithRLS } from './lib/rls-helpers'; - -// Queries - automatic filtering -export const listItems = queryWithRLS({ - args: { organizationId: v.id('organizations') }, - returns: v.array(v.any()), - handler: async (ctx, args) => { - return await ctx.db - .query('items') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - }, -}); - -// Mutations - automatic validation -export const createItem = mutationWithRLS({ - args: { organizationId: v.id('organizations'), name: v.string() }, - returns: v.id('items'), - handler: async (ctx, args) => { - return await ctx.db.insert('items', { - organizationId: args.organizationId, - name: args.name, - }); - }, -}); -``` - -This gives you bulletproof Row Level Security with minimal code and zero boilerplate. - -## Reference - -- [Stack Article: Row Level Security](https://stack.convex.dev/row-level-security) -- [convex-helpers Documentation](https://github.com/get-convex/convex-helpers) -- Implementation: `convex/lib/rls-helpers.ts` diff --git a/docs/tale-db-deployment.md b/docs/tale-db-deployment.md deleted file mode 100644 index c96b0da884..0000000000 --- a/docs/tale-db-deployment.md +++ /dev/null @@ -1,574 +0,0 @@ -# Tale DB Deployment Guide - -This guide covers deploying Tale DB in various environments, from local development to production Kubernetes clusters. - -## Table of Contents - -- [Local Development](#local-development) -- [Docker Compose Deployment](#docker-compose-deployment) -- [Kubernetes Deployment](#kubernetes-deployment) -- [Cloud Deployments](#cloud-deployments) -- [Security Hardening](#security-hardening) -- [Monitoring and Alerting](#monitoring-and-alerting) -- [Backup and Recovery](#backup-and-recovery) -- [Performance Tuning](#performance-tuning) - -## Local Development - -### Quick Start - -```bash -# Clone the repository -git clone https://github.com/your-org/tale.git -cd tale - -# Configure environment -cp db/.env.example .env -nano .env # Change DB_PASSWORD - -# Start Tale DB -docker compose up -d db - -# Verify it's running -docker compose ps db -docker compose logs -f db -``` - -### Development Configuration - -For local development, use minimal resource allocation: - -```bash -# .env for development -DB_NAME=tale_dev -DB_USER=tale_dev -DB_PASSWORD=dev_password_123 - -# Memory settings for development (2GB RAM available) -DB_SHARED_BUFFERS=128MB -DB_EFFECTIVE_CACHE_SIZE=512MB -DB_WORK_MEM=4MB - -# Enable verbose logging -DB_LOG_STATEMENT=all -DB_LOG_MIN_DURATION_STATEMENT=0 -``` - -## Docker Compose Deployment - -### Production Docker Compose - -For production deployments using Docker Compose: - -```yaml -# docker-compose.prod.yml -version: '3.8' - -services: - db: - image: ghcr.io/your-org/tale-db:latest - container_name: tale-db - restart: always - ports: - - '127.0.0.1:5432:5432' # Bind to localhost only - environment: - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - DB_MAX_CONNECTIONS: 200 - DB_SHARED_BUFFERS: 2GB - DB_EFFECTIVE_CACHE_SIZE: 6GB - DB_MAINTENANCE_WORK_MEM: 512MB - DB_WORK_MEM: 16MB - DB_LOG_STATEMENT: none - DB_LOG_MIN_DURATION_STATEMENT: 1000 - volumes: - - db-data:/var/lib/postgresql/data - - db-backup:/var/lib/postgresql/backup - - /etc/localtime:/etc/localtime:ro - networks: - - internal - deploy: - resources: - limits: - cpus: '4' - memory: 8G - reservations: - cpus: '2' - memory: 4G - -volumes: - db-data: - driver: local - db-backup: - driver: local - -networks: - internal: - driver: bridge -``` - -### Deploy - -```bash -# Set environment variables -export DB_PASSWORD=$(openssl rand -base64 32) - -# Start services -docker compose -f docker-compose.prod.yml up -d - -# Check health -docker compose -f docker-compose.prod.yml ps -docker compose -f docker-compose.prod.yml exec tale-db pg_isready -``` - -## Kubernetes Deployment - -### Namespace - -```yaml -# namespace.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: tale -``` - -### Secret - -```yaml -# secret.yaml -apiVersion: v1 -kind: Secret -metadata: - name: db-credentials - namespace: tale -type: Opaque -stringData: - DB_NAME: tale - DB_USER: tale - DB_PASSWORD: -``` - -### PersistentVolumeClaim - -```yaml -# pvc.yaml -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: db-data - namespace: tale -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Gi - storageClassName: fast-ssd # Adjust based on your cluster -``` - -### Deployment - -```yaml -# deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: db - namespace: tale -spec: - replicas: 1 - selector: - matchLabels: - app: db - template: - metadata: - labels: - app: db - spec: - containers: - - name: db - image: ghcr.io/your-org/tale-db:latest - ports: - - containerPort: 5432 - name: postgresql - env: - - name: DB_NAME - valueFrom: - secretKeyRef: - name: db-credentials - key: DB_NAME - - name: DB_USER - valueFrom: - secretKeyRef: - name: db-credentials - key: DB_USER - - name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: db-credentials - key: DB_PASSWORD - - name: DB_MAX_CONNECTIONS - value: '200' - - name: DB_SHARED_BUFFERS - value: '2GB' - - name: DB_EFFECTIVE_CACHE_SIZE - value: '6GB' - volumeMounts: - - name: data - mountPath: /var/lib/postgresql/data - resources: - requests: - memory: '4Gi' - cpu: '2' - limits: - memory: '8Gi' - cpu: '4' - livenessProbe: - exec: - command: - - pg_isready - - -U - - tale - - -d - - tale - initialDelaySeconds: 60 - periodSeconds: 30 - readinessProbe: - exec: - command: - - pg_isready - - -U - - tale - - -d - - tale - initialDelaySeconds: 30 - periodSeconds: 10 - volumes: - - name: data - persistentVolumeClaim: - claimName: db-data -``` - -### Service - -```yaml -# service.yaml -apiVersion: v1 -kind: Service -metadata: - name: db - namespace: tale -spec: - type: ClusterIP - ports: - - port: 5432 - targetPort: 5432 - protocol: TCP - name: postgresql - selector: - app: db -``` - -### Deploy to Kubernetes - -```bash -# Create namespace -kubectl apply -f namespace.yaml - -# Create secret (generate password first) -export DB_PASSWORD=$(openssl rand -base64 32) -kubectl create secret generic db-credentials \ - --from-literal=DB_NAME=tale \ - --from-literal=DB_USER=tale \ - --from-literal=DB_PASSWORD=$DB_PASSWORD \ - -n tale - -# Create PVC -kubectl apply -f pvc.yaml - -# Deploy database -kubectl apply -f deployment.yaml -kubectl apply -f service.yaml - -# Check status -kubectl get pods -n tale -kubectl logs -f deployment/db -n tale -``` - -## Cloud Deployments - -### AWS (ECS/Fargate) - -Use the GitHub Container Registry image with ECS task definitions: - -```json -{ - "family": "tale-db", - "networkMode": "awsvpc", - "requiresCompatibilities": ["FARGATE"], - "cpu": "2048", - "memory": "8192", - "containerDefinitions": [ - { - "name": "tale-db", - "image": "ghcr.io/your-org/tale-db:latest", - "portMappings": [ - { - "containerPort": 5432, - "protocol": "tcp" - } - ], - "environment": [ - { "name": "DB_MAX_CONNECTIONS", "value": "200" }, - { "name": "DB_SHARED_BUFFERS", "value": "2GB" } - ], - "secrets": [ - { - "name": "DB_PASSWORD", - "valueFrom": "arn:aws:secretsmanager:region:account:secret:tale-db-password" - } - ], - "mountPoints": [ - { - "sourceVolume": "tale-db-data", - "containerPath": "/var/lib/postgresql/data" - } - ] - } - ], - "volumes": [ - { - "name": "tale-db-data", - "efsVolumeConfiguration": { - "fileSystemId": "fs-xxxxx" - } - } - ] -} -``` - -### Google Cloud (Cloud Run) - -```bash -# Build and push to GCR -docker tag tale-db:latest gcr.io/your-project/tale-db:latest -docker push gcr.io/your-project/tale-db:latest - -# Deploy to Cloud Run -gcloud run deploy tale-db \ - --image gcr.io/your-project/tale-db:latest \ - --platform managed \ - --region us-central1 \ - --memory 8Gi \ - --cpu 4 \ - --set-env-vars DB_MAX_CONNECTIONS=200 \ - --set-secrets DB_PASSWORD=tale-db-password:latest -``` - -### Azure (Container Instances) - -```bash -# Create resource group -az group create --name tale-rg --location eastus - -# Create container -az container create \ - --resource-group tale-rg \ - --name tale-db \ - --image ghcr.io/your-org/tale-db:latest \ - --cpu 4 \ - --memory 8 \ - --ports 5432 \ - --environment-variables \ - DB_MAX_CONNECTIONS=200 \ - DB_SHARED_BUFFERS=2GB \ - --secure-environment-variables \ - DB_PASSWORD=$DB_PASSWORD -``` - -## Security Hardening - -### 1. Network Security - -```bash -# Bind to localhost only -ports: - - "127.0.0.1:5432:5432" - -# Use internal networks -networks: - - tale-internal -``` - -### 2. SSL/TLS Configuration - -Create SSL certificates and mount them: - -```yaml -volumes: - - ./certs/server.crt:/var/lib/postgresql/server.crt:ro - - ./certs/server.key:/var/lib/postgresql/server.key:ro -``` - -### 3. Strong Passwords - -```bash -# Generate secure password -openssl rand -base64 32 - -# Use secrets management -# - Kubernetes Secrets -# - AWS Secrets Manager -# - Azure Key Vault -# - HashiCorp Vault -``` - -### 4. Firewall Rules - -```bash -# Allow only specific IPs -iptables -A INPUT -p tcp --dport 5432 -s 10.0.0.0/8 -j ACCEPT -iptables -A INPUT -p tcp --dport 5432 -j DROP -``` - -## Monitoring and Alerting - -### Prometheus Exporter - -```yaml -# Add postgres_exporter sidecar -- name: postgres-exporter - image: prometheuscommunity/postgres-exporter - ports: - - containerPort: 9187 - env: - - name: DATA_SOURCE_NAME - value: 'postgresql://tale:password@localhost:5432/tale?sslmode=disable' -``` - -### Grafana Dashboard - -Import dashboard ID: 9628 (PostgreSQL Database) - -### CloudWatch (AWS) - -```bash -# Enable CloudWatch logs -awslogs-group: /ecs/tale-db -awslogs-region: us-east-1 -awslogs-stream-prefix: tale-db -``` - -## Backup and Recovery - -### Automated Backups - -```bash -# Cron job for daily backups -0 2 * * * docker exec tale-db pg_dump -U tale tale | gzip > /backups/tale-$(date +\%Y\%m\%d).sql.gz - -# Retention policy (keep 30 days) -find /backups -name "tale-*.sql.gz" -mtime +30 -delete -``` - -### Point-in-Time Recovery - -Enable WAL archiving: - -```bash -# Add to postgresql.conf -wal_level = replica -archive_mode = on -archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f' -``` - -## Performance Tuning - -### Connection Pooling - -Use PgBouncer: - -```yaml -services: - pgbouncer: - image: pgbouncer/pgbouncer - environment: - DATABASES_HOST: tale-db - DATABASES_PORT: 5432 - DATABASES_USER: tale - DATABASES_PASSWORD: ${DB_PASSWORD} - DATABASES_DBNAME: tale - PGBOUNCER_POOL_MODE: transaction - PGBOUNCER_MAX_CLIENT_CONN: 1000 - PGBOUNCER_DEFAULT_POOL_SIZE: 25 -``` - -### Query Optimization - -```sql --- Enable query statistics -CREATE EXTENSION pg_stat_statements; - --- Find slow queries -SELECT - query, - calls, - total_time, - mean_time, - max_time -FROM pg_stat_statements -ORDER BY mean_time DESC -LIMIT 10; -``` - -### Index Optimization - -```sql --- Find missing indexes -SELECT - schemaname, - tablename, - attname, - n_distinct, - correlation -FROM pg_stats -WHERE schemaname = 'tale' -ORDER BY n_distinct DESC; -``` - -## Troubleshooting - -### Common Issues - -1. **Out of Memory** - - - Reduce `shared_buffers` - - Reduce `work_mem` - - Add more RAM - -2. **Too Many Connections** - - - Increase `max_connections` - - Use connection pooling - - Check for connection leaks - -3. **Slow Queries** - - - Add indexes - - Optimize queries - - Increase `work_mem` - -4. **Disk Space** - - Enable autovacuum - - Archive old data - - Increase volume size - -## Support - -For issues or questions: - -- Check [Tale DB README](../services/db/README.md) -- Review [TimescaleDB docs](https://docs.timescale.com/) -- Contact Tale support team diff --git a/docs/tale-rag-deployment.md b/docs/tale-rag-deployment.md deleted file mode 100644 index d18e315f23..0000000000 --- a/docs/tale-rag-deployment.md +++ /dev/null @@ -1,348 +0,0 @@ -# Tale RAG Deployment Guide - -This guide covers the deployment of the Tale RAG (Retrieval-Augmented Generation) service. - -## Overview - -Tale RAG is a production-ready RAG service built with cognee and FastAPI. It provides semantic search, document management, and AI-powered response generation capabilities. - -## Quick Start - -### Prerequisites - -- Docker and Docker Compose -- OpenAI API key (or other LLM provider) -- Tale DB (PostgreSQL with PGVector) running - -### Start the Service - -```bash -# Set your OpenAI API key -export OPENAI_API_KEY=sk-your-api-key-here - -# Start all Tale services including RAG -docker compose up -d - -# Or start only the RAG service -docker compose up -d rag -``` - -### Verify the Service - -```bash -# Check health -curl http://localhost:8001/health - -# View API documentation -open http://localhost:8001/docs -``` - -## Configuration - -### Environment Variables - -Configuration uses service-prefixed variables for service internals and general variables for cross-service URLs. Key variables include: - -#### Required - -- `OPENAI_API_KEY` - OpenAI API key for LLM and embeddings -- `DATABASE_URL` - PostgreSQL connection URL (also used for PGVector storage) - -#### Server - -- `RAG_HOST` - Server host (default: `0.0.0.0`) -- `RAG_PORT` - Server port (default: `8001`) -- `RAG_WORKERS` - Number of workers (default: `1`) -- `RAG_LOG_LEVEL` - Log level (default: `info`) - -#### Cognee - -- `RAG_CHUNK_SIZE` - Document chunk size (default: `512`) -- `RAG_CHUNK_OVERLAP` - Chunk overlap (default: `50`) -- `RAG_TOP_K` - Number of results (default: `5`) -- `RAG_SIMILARITY_THRESHOLD` - Minimum similarity (default: `0.7`) -- `EMBEDDING_DIMENSIONS` - Embedding vector dimension size. When using custom - embedding models or providers, this must match your vector store's configured - dimension. - -See [services/rag/README.md](../services/rag/README.md) for complete documentation. - -## API Endpoints - -### Document Management - -- `POST /api/v1/documents` - Add a document -- `DELETE /api/v1/documents/{id}` - Delete a document -- `POST /api/v1/documents/batch` - Batch add documents - -### Query & Search - -- `POST /api/v1/search` - Semantic search -- `POST /api/v1/generate` - Generate RAG response - -### System - -- `GET /health` - Health check -- `GET /config` - Configuration -- `GET /docs` - API documentation - -## Usage Examples - -### Add a Document - -```bash -curl -X POST http://localhost:8001/api/v1/documents \ - -H "Content-Type: application/json" \ - -d '{ - "content": "Tale RAG provides powerful semantic search capabilities.", - "metadata": {"source": "docs", "category": "features"}, - "document_id": "doc-001" - }' -``` - -### Search - -```bash -curl -X POST http://localhost:8001/api/v1/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "What are the features of Tale RAG?", - "top_k": 5 - }' -``` - -### Generate Response - -```bash -curl -X POST http://localhost:8001/api/v1/generate \ - -H "Content-Type: application/json" \ - -d '{ - "query": "Explain the capabilities of Tale RAG", - "top_k": 3 - }' -``` - -## Docker Deployment - -### Using Docker Compose - -The recommended deployment method: - -```bash -# Start with all Tale services -docker compose up -d - -# View logs -docker compose logs -f rag - -# Stop the service -docker compose down -``` - -### Using Docker Directly - -```bash -# Build the image -docker build -t tale-rag:latest -f services/rag/Dockerfile . - -# Run the container -docker run -d \ - -p 8001:8001 \ - -e OPENAI_API_KEY=sk-... \ - -e DATABASE_URL=postgresql://user:pass@host:5432/db \ - --name tale-rag \ - tale-rag:latest -``` - -### Multi-Architecture Build - -```bash -# Build for AMD64 and ARM64 -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - -t tale-rag:latest \ - -f services/rag/Dockerfile \ - . -``` - -## GitHub Container Registry - -### Pull Pre-built Image - -```bash -# Pull from GHCR -docker pull ghcr.io/your-org/tale/tale-rag:latest - -# Run the image -docker run -d \ - -p 8001:8001 \ - -e OPENAI_API_KEY=sk-... \ - ghcr.io/your-org/tale/tale-rag:latest -``` - -### Build and Push - -The GitHub Actions workflow automatically builds and pushes images: - -```bash -# Manual trigger -gh workflow run build-and-push-rag.yml - -# Or push a tag -git tag rag-v1.0.0 -git push origin rag-v1.0.0 -``` - -## Integration with Tale Platform - -### Service Dependencies - -Tale RAG integrates with: - -1. **Tale DB (PostgreSQL)** - Stores metadata, system data, and vector embeddings (via PGVector) -2. **Tale Graph DB (Kuzu)** - Optional knowledge graph storage - -### Network Configuration - -All services communicate via the `internal` Docker network: - -```yaml -networks: - internal: - driver: bridge -``` - -### Volume Management - -Persistent data is stored in: - -- `rag-data` - Cognee data directory - -## Production Deployment - -### Security - -```bash -# Set strong API keys -export RAG_API_KEY=$(openssl rand -base64 32) -export RAG_SECRET_KEY=$(openssl rand -base64 32) - -# Restrict CORS -export RAG_ALLOWED_ORIGINS=https://yourdomain.com - -# Use secure database connections -export DATABASE_URL=postgresql://user:pass@db:5432/production?sslmode=require -``` - -### Performance - -```bash -# Increase workers for high traffic -export RAG_WORKERS=4 - -# Optimize database connections -export RAG_DATABASE_POOL_SIZE=20 -export RAG_DATABASE_MAX_OVERFLOW=40 - -# Increase concurrent requests -export RAG_MAX_CONCURRENT_REQUESTS=50 -``` - -### Monitoring - -```bash -# Enable metrics -export RAG_ENABLE_METRICS=true -export RAG_PROMETHEUS_ENABLED=true - -# Enable error tracking -export RAG_SENTRY_DSN=https://... -``` - -## Troubleshooting - -### Service Won't Start - -```bash -# Check logs -docker compose logs rag - -# Verify environment -docker compose exec rag env | grep RAG_ - -# Check dependencies -docker compose ps -``` - -### Connection Issues - -```bash -# Test database connection -docker compose exec rag python -c "import asyncpg; print('DB OK')" - -# Test vector database -curl http://localhost:6333/healthz - -# Check network -docker network inspect tale_internal -``` - -### Performance Issues - -```bash -# Monitor resources -docker stats tale-rag - -# Check database connections -docker compose exec db psql -U tale -c "SELECT count(*) FROM pg_stat_activity;" - -# Review logs -docker compose logs --tail=100 rag -``` - -## Maintenance - -### Backup - -```bash -# Backup cognee data -docker run --rm \ - -v tale_rag-data:/data \ - -v $(pwd)/backups:/backup \ - alpine tar czf /backup/rag-data-$(date +%Y%m%d).tar.gz /data -``` - -### Updates - -```bash -# Pull latest image -docker compose pull rag - -# Restart service -docker compose up -d rag -``` - -### Clean Up - -```bash -# Remove old containers -docker compose down - -# Remove volumes (WARNING: deletes data) -docker compose down -v -``` - -## Additional Resources - -- [README.md](../services/rag/README.md) - Complete documentation -- [QUICKSTART.md](../services/rag/QUICKSTART.md) - Quick start guide -- [IMPLEMENTATION_SUMMARY.md](../services/rag/IMPLEMENTATION_SUMMARY.md) - Technical details -- API Documentation: http://localhost:8001/docs - -## Support - -For issues and questions: - -- GitHub Issues: [Create an issue](https://github.com/your-org/tale/issues) -- Documentation: http://localhost:8001/docs -- Health Check: http://localhost:8001/health diff --git a/docs/tone-of-voice-implementation.md b/docs/tone-of-voice-implementation.md deleted file mode 100644 index 0b75767fa8..0000000000 --- a/docs/tone-of-voice-implementation.md +++ /dev/null @@ -1,278 +0,0 @@ -# Tone of Voice Module Implementation - -## Overview - -The Tone of Voice module enables organizations to define and maintain their brand voice through example messages and AI-generated tone descriptions. - -## Schema Design - -### Tables - -#### 1. `toneOfVoice` - -Stores the organization's tone of voice configuration. - -**Fields:** - -- `organizationId`: ID reference to organizations table -- `generatedTone`: AI-generated tone description (optional) -- `lastUpdated`: Timestamp of last update -- `metadata`: Flexible metadata field - -**Indexes:** - -- `by_organizationId`: Query tone of voice by organization - -#### 2. `exampleMessages` - -Stores example messages used to generate tone of voice. - -**Fields:** - -- `organizationId`: ID reference to organizations table -- `toneOfVoiceId`: ID reference to toneOfVoice table -- `content`: Message content -- `createdAt`: Creation timestamp -- `updatedAt`: Last update timestamp -- `metadata`: Flexible metadata field - -**Indexes:** - -- `by_organizationId`: Query examples by organization -- `by_toneOfVoiceId`: Query examples by tone of voice -- `by_organizationId_and_toneOfVoiceId`: Combined index for efficient queries - ---- - -## API Reference - -### Tone of Voice APIs (`convex/tone_of_voice.ts`) - -#### Queries - -##### `getToneOfVoice` - -Get tone of voice for an organization. - -```typescript -const toneOfVoice = await useQuery(api.tone_of_voice.getToneOfVoice, { - organizationId: 'j9...', -}); -``` - -**Returns:** Tone of voice object or null - -##### `getExampleMessages` - -Get example messages for a tone of voice. - -```typescript -const examples = await useQuery(api.tone_of_voice.getExampleMessages, { - organizationId: 'j9...', - toneOfVoiceId: 'j9...', // Optional -}); -``` - -**Returns:** Array of example messages - -##### `getToneOfVoiceWithExamples` - -Get tone of voice with all example messages. - -```typescript -const data = await useQuery(api.tone_of_voice.getToneOfVoiceWithExamples, { - organizationId: 'j9...', -}); -``` - -**Returns:** Object with `toneOfVoice` and `examples` or null - -#### Mutations - -##### `upsertToneOfVoice` - -Create or update tone of voice. - -```typescript -const toneId = await useMutation(api.tone_of_voice.upsertToneOfVoice, { - organizationId: 'j9...', - generatedTone: 'Friendly and professional...', - metadata: {}, // Optional -}); -``` - -**Returns:** Tone of voice ID - -##### `addExampleMessage` - -Add an example message. - -```typescript -const messageId = await useMutation(api.tone_of_voice.addExampleMessage, { - organizationId: 'j9...', - content: 'Thank you for your purchase!', - metadata: {}, // Optional -}); -``` - -**Returns:** Example message ID - -##### `updateExampleMessage` - -Update an example message. - -```typescript -await useMutation(api.tone_of_voice.updateExampleMessage, { - messageId: 'j9...', - content: 'Updated content', // Optional - metadata: {}, // Optional -}); -``` - -##### `deleteExampleMessage` - -Delete an example message. - -```typescript -await useMutation(api.tone_of_voice.deleteExampleMessage, { - messageId: 'j9...', -}); -``` - -#### Actions - -##### `generateToneOfVoice` - -Generate tone of voice from example messages using AI. - -```typescript -const result = await useAction(api.tone_of_voice.generateToneOfVoice, { - organizationId: 'j9...', -}); - -if (result.success) { - console.log('Generated tone:', result.tone); -} else { - console.error('Error:', result.error); -} -``` - -**Returns:** Object with `success`, `tone` (if successful), and `error` (if failed) - -**Requirements:** - -- At least one example message must exist -- `OPENAI_API_KEY` environment variable must be set - -##### `regenerateToneOfVoice` - -Regenerate tone of voice (same as generate). - -```typescript -const result = await useAction(api.tone_of_voice.regenerateToneOfVoice, { - organizationId: 'j9...', -}); -``` - ---- - -## Usage Examples - -### Example 1: Setting up tone of voice - -```typescript -'use client'; - -import { useMutation, useQuery, useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; - -function ToneOfVoiceSetup({ organizationId }) { - const toneData = useQuery(api.tone_of_voice.getToneOfVoiceWithExamples, { - organizationId, - }); - const addExample = useMutation(api.tone_of_voice.addExampleMessage); - const generateTone = useAction(api.tone_of_voice.generateToneOfVoice); - - const handleAddExample = async (content: string) => { - await addExample({ - organizationId, - content, - }); - }; - - const handleGenerateTone = async () => { - const result = await generateTone({ organizationId }); - if (result.success) { - console.log('Generated tone:', result.tone); - } else { - console.error('Error:', result.error); - } - }; - - return ( -
- {/* UI implementation */} -
- ); -} -``` - ---- - -## Environment Variables - -### Required - -- `OPENAI_API_KEY`: OpenAI API key for AI tone generation - -### Setup - -Add to your `.env.local` file: - -```bash -OPENAI_API_KEY=your_openai_api_key_here -``` - ---- - -## AI Model Configuration - -The tone generation uses OpenAI with GPT-4o: - -```typescript -const result = await generateObject({ - model: openai('gpt-4o'), - // ... -}); -``` - -You can change the model by modifying the `model` parameter in `convex/tone_of_voice.ts`. - ---- - -## Best Practices - -1. **Example Messages**: Add at least 3-5 diverse examples for better tone generation -2. **Content Quality**: Use authentic messages that truly represent your brand voice -3. **Regular Updates**: Regenerate tone periodically as your brand voice evolves - ---- - -## Troubleshooting - -### Issue: "No example messages found" - -**Solution:** Add at least one example message before generating tone - -### Issue: "OpenAI API key not configured" - -**Solution:** Ensure `OPENAI_API_KEY` is set in environment variables - ---- - -## Future Enhancements - -- Support for more AI models -- Tone versioning and history -- Tone comparison tools -- Export/import functionality diff --git a/docs/url-configuration.md b/docs/url-configuration.md deleted file mode 100644 index 54a24e22a0..0000000000 --- a/docs/url-configuration.md +++ /dev/null @@ -1,231 +0,0 @@ -# URL Configuration Guide - -This document explains how URLs are configured in the Tale Platform and how they're derived at runtime. - -## Architecture Overview - -The Tale Platform uses a **runtime URL derivation strategy** to avoid baking environment-specific values into Docker images: - -1. **Client-side (Browser)**: Derives URLs from `window.location.origin` at runtime -2. **Server-side (Next.js)**: Uses `SITE_URL` environment variable -3. **Backend-to-Backend**: Uses internal Docker network addresses - -**Key Principle**: No `NEXT_PUBLIC_*` environment variables are used. This ensures Docker images are portable across environments. - -## URL Configuration - -### Setting the DOMAIN Variable - -The `DOMAIN` environment variable is the single source of truth for public URLs in Docker Compose: - -```bash -# Local development (HTTP) -DOMAIN=http://localhost - -# Production with domain name (HTTPS) -DOMAIN=https://demo.tale.dev - -# Production with IP address (HTTP) -DOMAIN=http://203.0.113.10 -``` - -If you omit the protocol, `http://` will be added automatically as the default. - -### How Caddy Handles HTTPS - -Caddy's automatic HTTPS behavior varies by domain type: - -1. **localhost/127.0.0.1**: - - - Caddy serves with HTTPS using **self-signed certificates** - - Requires trusting Caddy's root CA (Caddy will attempt to install it automatically) - - If you set `DOMAIN=http://localhost`, clients connect via HTTP, but Caddy may redirect to HTTPS - - You may see browser certificate warnings if Caddy's root CA is not trusted - -2. **Domain names** (e.g., demo.tale.dev): - - - Caddy automatically obtains certificates from Let's Encrypt or ZeroSSL via ACME - - Serves over HTTPS with valid, publicly-trusted certificates - - Set `DOMAIN=https://demo.tale.dev` for production - -3. **IP addresses** (e.g., 203.0.113.10): - - - Caddy automatically disables TLS (serves over HTTP) - - ACME providers don't issue certificates for IP addresses - - Set `DOMAIN=http://203.0.113.10` - -**For local development**: If you want to avoid certificate warnings with localhost, you can either: - -- Trust Caddy's root CA (run `caddy trust` as admin) -- Or modify the Caddyfile to use `http://localhost` explicitly to disable HTTPS - -## Runtime URL Derivation - -### Client-Side (Browser) - -The Convex client derives URLs at runtime from `window.location.origin`: - -```typescript -// services/platform/components/convex-auth-provider.tsx -function getConvexUrl(): string { - return `${window.location.origin}/ws_api`; -} -``` - -This means: -- **No `NEXT_PUBLIC_*` variables needed** -- Docker images work in any environment without rebuild -- URLs automatically match the domain the user is accessing - -### Server-Side (Next.js) - -Server-side code uses the `SITE_URL` environment variable: - -```typescript -// services/platform/lib/convex-next-server.ts -const rawSiteUrl = process.env.SITE_URL || 'http://localhost:3000'; -const url = `${rawSiteUrl.replace(/\/+$/, '')}/ws_api`; -``` - -### Environment Variables - -| Variable | Scope | Purpose | -|----------|-------|---------| -| `SITE_URL` | Server-side | Base URL for the platform (e.g., `https://demo.tale.dev`) | -| `DOMAIN` | Docker Compose | Passed to Caddy and derived to `SITE_URL` | - -**Note**: `NEXT_PUBLIC_*` variables are intentionally NOT used to keep Docker images environment-agnostic. - -## Configuration Flow - -### 1. Environment Variables in compose.yml - -The `compose.yml` file passes the `DOMAIN` variable to the platform service: - -```yaml -environment: - DOMAIN: ${DOMAIN:-localhost} -``` - -### 2. URL Derivation in env.sh - -The `services/platform/env.sh` script derives `SITE_URL` from the `DOMAIN` variable: - -```bash -# Domain configuration - auto-derive SITE_URL -local base_url="${DOMAIN:-http://localhost}" - -# Ensure DOMAIN includes a protocol -if [[ ! "$base_url" =~ ^https?:// ]]; then - base_url="http://${base_url}" -fi - -# Site URL - the canonical base URL for the platform -export SITE_URL="${SITE_URL:-${base_url}}" -``` - -## Examples - -### Local Development - -```bash -# .env -DOMAIN=http://localhost -``` - -Results in: -- `SITE_URL=http://localhost:3000` (server-side) -- Browser automatically uses `http://localhost:3000/ws_api` for Convex - -### Production with Domain - -```bash -# .env -DOMAIN=https://demo.tale.dev -``` - -Results in: -- `SITE_URL=https://demo.tale.dev` (server-side) -- Browser automatically uses `https://demo.tale.dev/ws_api` for Convex - -### Production with IP Address - -```bash -# .env -DOMAIN=http://203.0.113.10 -``` - -Results in: -- `SITE_URL=http://203.0.113.10` (server-side) -- Browser automatically uses `http://203.0.113.10/ws_api` for Convex - -## Proxy Configuration - -The Caddy proxy automatically handles: - -1. **HTTP for localhost**: No TLS, direct HTTP access -2. **HTTPS for domains**: Automatic certificate provisioning via ACME -3. **HTTP for IP addresses**: No TLS (ACME doesn't support IP addresses) - -The proxy routes are configured in `services/proxy/Caddyfile`: - -``` -{$DOMAIN:localhost} { - # Convex WebSocket API - handle_path /ws_api* { - reverse_proxy http://platform:3210 - } - - # Convex HTTP Actions API - handle_path /http_api* { - reverse_proxy http://platform:3211 - } - - # Main application - handle { - reverse_proxy http://platform:3000 - } -} -``` - -## Troubleshooting - -### Issue: Client can't connect to Convex - -**Check**: - -1. Verify `DOMAIN` is set correctly in `.env` -2. Verify the proxy is running and accessible -3. Check browser console for WebSocket connection errors -4. Ensure the `/ws_api` path is being proxied correctly - -### Issue: Platform service can't connect to Convex backend - -**Check**: - -1. Verify `SITE_URL` is set correctly -2. Check that the Convex backend is running on port 3210 -3. Verify Next.js rewrites are configured in `next.config.ts` - -### Issue: HTTPS not working - -**Check**: - -1. Verify `DOMAIN` is set to a valid domain name (not localhost or IP) -2. Check that DNS is pointing to your server -3. Verify ports 80 and 443 are open -4. Check Caddy logs for certificate provisioning errors - -## Why No NEXT_PUBLIC_* Variables? - -Next.js injects `NEXT_PUBLIC_*` environment variables at **build time** into the client JavaScript bundle. This creates a problem for Docker deployments: - -- Images built with `NEXT_PUBLIC_CONVEX_URL=https://staging.example.com` cannot be reused for production -- You would need to rebuild the image for each environment - -By deriving URLs from `window.location.origin` at runtime, the same Docker image works in any environment. - -## Related Documentation - -- [Convex Self-Hosted Setup](./convex-self-hosted-setup.md) -- [Configuration Guide](../CONFIGURATION.md) diff --git a/docs/workflows/REFACTORING_SUMMARY.md b/docs/workflows/REFACTORING_SUMMARY.md deleted file mode 100644 index 68108aaf33..0000000000 --- a/docs/workflows/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,232 +0,0 @@ -# Workflow Module Refactoring Summary - -## Overview - -Successfully refactored the workflow module to distinguish between two main workflow types and implemented shared thread context for dynamic orchestration workflows. - -## Key Changes - -### 1. Workflow Type Classification - -**Two Main Types:** - -1. **Predefined Workflows** (`workflowType: 'predefined'`) - - - Developer-defined workflows for platform integrations and data operations - - Users can't create their own, but can choose which to use - - Can include any node types (action, LLM, agent, condition, loop, etc.) - - Each LLM/agent step creates its own thread (no shared context) - -2. **Dynamic Orchestration Workflows** (`workflowType: 'dynamic_orchestration'`) - - User-defined or AI-generated workflows - - All LLM steps share a single threadId - - Focus on single objects (e.g., one customer) - - Pure structure: 1 trigger + multiple LLM nodes + 1 finish trigger - -### 2. Pure Dynamic Orchestration Structure - -**Required Structure:** - -- ✅ 1 Trigger node (defines when to run) -- ✅ Multiple LLM nodes (each representing a different agent role) -- ✅ 1 Finish trigger node -- ❌ No action, condition, or loop nodes - -**Example: Single Customer Status Assessment** - -``` -Trigger → Data Collector Agent → Status Analyzer Agent → Update Executor Agent → Finish -``` - -### 3. Shared Thread Context - -**Key Innovation:** -All agents in an agent orchestration workflow share the same threadId, enabling: - -- **Natural conversation flow** - Agents collaborate like a team discussing the same case -- **Context continuity** - Each agent can access previous agents' outputs through conversation history -- **Simpler prompts** - No need to explicitly pass data between agents -- **Complete audit trail** - All agent interactions are recorded in one thread - -**Before (unnecessary):** - -```typescript -userPrompt: 'Analyze this data: {{steps.data_collector_agent.output.data}}'; -``` - -**After (leverages shared thread):** - -```typescript -userPrompt: 'Based on the customer data from the Data Collector Agent, analyze and determine status.'; -``` - -## Files Modified - -### Schema Changes - -- ✅ `convex/schema.ts` - Added `workflowType` and `threadId` fields - -### Type Definitions - -- ✅ `convex/workflow/types/workflow.ts` - Added workflow type definitions -- ✅ `convex/workflow/types/workflow_types.ts` - Created helper functions -- ✅ `convex/workflow/types/inline.ts` - Updated inline workflow config - -### Workflow Execution - -- ✅ `convex/workflow/engine/execute_workflow_start.ts` - Shared helper that starts workflows for existing `wfExecutions` records -- ✅ `convex/workflow/engine/dynamic_workflow.ts` - Passes threadId through steps -- ✅ `convex/workflow/core/step_execution/execute_step_by_type.ts` - Passes threadId to LLM nodes - -### LLM Node Executor - -- ✅ `convex/workflow/nodes/llm/execute_llm_node.ts` - Accepts threadId parameter -- ✅ `convex/workflow/nodes/llm/execute_agent_with_tools.ts` - Reuses threadId for agent orchestration -- ✅ `convex/workflow/nodes/llm/types.ts` - Added threadId to result type - -### Workflow Definitions - -**Data Sync Workflows:** - -- ✅ `workflows/shopify-sync-products.ts` -- ✅ `workflows/shopify-sync-customers.ts` -- ✅ `workflows/circuly-sync-customers.ts` -- ✅ `workflows/circuly-sync-products.ts` -- ✅ `workflows/circuly-sync-subscriptions.ts` - -**Agent Orchestration Workflows:** - -- ✅ `workflows/assess-customer-status.ts` - **Refactored to pure agent orchestration** -- ✅ `workflows/product-recommendation.ts` -- ✅ `workflows/product-relationship-analysis.ts` -- ✅ `workflows/send-churn-survey-email.ts` -- ✅ `workflows/find-product-by-shop-variant-id.ts` - -### Documentation - -- ✅ `docs/workflows/workflow-types.md` - Comprehensive documentation -- ✅ `docs/workflows/REFACTORING_SUMMARY.md` - This file - -## Example: assess-customer-status.ts Transformation - -### Before (Hybrid Workflow) - -- Mixed action, condition, loop, and LLM nodes -- Processed multiple customers with pagination -- Complex control flow with branching logic -- ~245 lines of code - -### After (Pure Agent Orchestration) - -- Only trigger and LLM nodes -- Focuses on ONE customer per execution -- Linear agent collaboration flow -- ~196 lines of code -- Cleaner, more maintainable structure - -**Agent Flow:** - -1. **Trigger** - Manual trigger with customerId input -2. **Data Collector Agent** - Retrieves customer data using `customer_search` tool -3. **Status Analyzer Agent** - Analyzes subscription patterns to determine status -4. **Update Executor Agent** - Updates database using `customer_update` tool -5. **Finish** - Completes workflow - -## Thread Management Flow - -### Agent Orchestration Workflow: - -1. Workflow starts → Create thread via `components.agent.threads.createThread` -2. Store `threadId` in `wfExecutions.threadId` -3. Pass `threadId` to all LLM steps -4. Each LLM step reuses the same thread → **Maintains conversation context** - -### Data Sync Workflow: - -1. Workflow starts → No thread created -2. Each LLM step (if any) creates its own thread → **Independent execution** - -## Benefits - -1. **Clear Separation of Concerns** - - - Data sync and agent orchestration are distinct concepts - - Each type is optimized for its specific use case - -2. **Shared Thread Context** - - - Agents maintain conversation context across steps - - Natural collaboration between agents - - Complete audit trail in one thread - -3. **Simpler Prompts** - - - No need to explicitly pass data between agents - - Agents reference previous context naturally - - More maintainable and readable - -4. **Better User Experience** - - - Users can easily identify which workflows they can customize - - Clear distinction between predefined and user-defined workflows - -5. **Improved Reliability** - - - Pure agent orchestration focuses on single objects - - Reduces complexity and potential for errors - - Easier to test and debug - -6. **Scalability** - - Clear boundaries make it easier to optimize each type independently - - Agent orchestration can be AI-generated through conversation - - Data sync workflows remain stable and predefined - -## Migration Guide - -### For Existing Workflows - -All existing workflow definitions have been updated to include the `workflowType` field: - -- **Predefined workflows**: `workflowType: 'predefined'` -- **Dynamic orchestration workflows**: `workflowType: 'dynamic_orchestration'` - -### For New Workflows - -When creating a new dynamic orchestration workflow: - -1. Set `workflowType: 'dynamic_orchestration'` -2. Use only trigger and LLM nodes -3. Focus on a single object per execution -4. Let agents reference previous context naturally (no explicit data passing) -5. Each agent should have a clear role and responsibility - -### Default Behavior - -If `workflowType` is not specified, it defaults to `'dynamic_orchestration'` for backward compatibility. - -## Next Steps - -1. **Test the Implementation** - - - Test data sync workflows still create individual threads per LLM step - - Test agent orchestration workflows share a single thread across all LLM steps - - Verify thread creation and reuse logic works correctly - -2. **Refactor Other Workflows** - - - Consider refactoring other hybrid workflows to pure agent orchestration - - Identify workflows that should remain hybrid (if any) - -3. **AI-Generated Workflows** - - - Implement conversation-based workflow generation - - Use the pure agent orchestration structure as the template - -4. **Performance Optimization** - - Monitor thread usage and performance - - Optimize agent prompts for better collaboration - - Consider caching strategies for frequently accessed data - -## Conclusion - -The workflow module has been successfully refactored to support two distinct workflow types with clear separation of concerns. The pure agent orchestration structure with shared thread context enables natural agent collaboration and simplifies workflow development. diff --git a/docs/workflows/architecture.md b/docs/workflows/architecture.md deleted file mode 100644 index fdea1a1536..0000000000 --- a/docs/workflows/architecture.md +++ /dev/null @@ -1,755 +0,0 @@ -# Workflow Module – MVP Development Guide - -## Current Table Structure - -To avoid collisions with the get-convex/workflow component’s internal table names, our definition layer uses prefixed table names: **wfDefinitions**, **wfStepDefs**, **wfExecutions**, **approvals**. - -- **wfDefinitions**: Workflow templates/definitions (UI-editable) -- **wfStepDefs**: Step definitions and configuration (UI-editable) -- **wfExecutions**: Execution instances and status (authoritative view in our app; includes a mapping to the component runtime) -- **approvals**: Approval work items (unified human-in-the-loop tasks) - -Note: For the execution layer we fully reuse the get-convex/workflow component (its internal tables: workflows/steps/config/onCompleteFailures). Responsibilities are clearly separated and non-overlapping; our execution records include a mapping field to the component-run workflow instance. - -## 1) Scope (MVP) - -- In scope - - Workflow definition via dedicated workflow tables - - Manual and scheduled (cron) triggers - - Execution engine (sequential, simple condition) - - Human approval node with pause/resume - - Execution journaling via the @convex-dev/workflow component (start/step/finish/error) - - Unified work management (replacing traditional tasks) - -## 2) Architecture at a glance - -- Separation of concerns - - wfDefinitions: workflow template definitions (UI editable) - - wfStepDefs: step definitions and configuration (UI editable) - - wfExecutions: execution status (authoritative in our app) - - approvals: human approval work items -- Data flow - 1. Define workflow template -> store in wfDefinitions - 2. Define workflow steps -> store in wfStepDefs - 3. Trigger (manual/cron/event) -> create wfExecutions -> delegate to component engine - 4. Component engine runs steps; our adapter decides next step/branch - 5. Approval step may pause (waitingFor='approval'); approver acts -> resume -> continue - -## 2.5) Execution layer via Convex Workflow Component (no table collisions) - -- We reuse the official execution engine (get-convex/workflow). Its own tables (workflows/steps/config/onCompleteFailures) are internal to the component. -- Our UI “definition layer” uses prefixed tables (wfDefinitions/wfStepDefs/wfExecutions/approvals), so names do not collide. -- Bridge/adapter responsibilities: - - - Start: create our wfExecutions, then create component workflow; store mapping in wfExecutions.componentWorkflowId - - Drive: component invokes our driver after each step; we decide next step from wfStepDefs and schedule the next component step - - Complete: component calls our completion hook; we mirror final status/output to wfExecutions - -- Manager integration (internal): Workflows can also be started via a WorkflowManager wrapper. The adapter exposes internal drivers `definitionDriver` and `onWorkflowComplete` used by the manager; behavior remains the same. - -Adapter functions (names to be implemented): - -- public mutation workflow.adapter.startWithComponent({ organizationId, workflowId, input, triggeredBy, triggerData }) - - returns: { executionId: Id<'wfExecutions'>, componentWorkflowId: string } -- internal mutation workflow.adapter.componentDriver({ workflowId, generationNumber }) - - loads wfStepDefs, chooses next step, updates wfExecutions, and calls components.journal.startStep(...) -- internal mutation workflow.adapter.onComponentComplete({ workflowId, generationNumber, runResult }) - - mirrors final status to wfExecutions - -Field mapping: - -- wfExecutions.componentWorkflowId: string (maps to component workflows.\_id) -- Index: by_component_workflow(componentWorkflowId) for quick lookup by component - -## 3) Unified data model (Convex schema) - -### wfDefinitions (workflow template definitions) - -```typescript -wfDefinitions: defineTable({ - organizationId: v.string(), // Better Auth organization ID - name: v.string(), - description: v.optional(v.string()), - - version: v.string(), // version control for workflow templates - status: v.string(), // 'draft' | 'active' | 'inactive' | 'archived' - - // Workflow-level configuration - config: v.optional( - v.object({ - timeout: v.optional(v.number()), - retryPolicy: v.optional( - v.object({ - maxRetries: v.number(), - backoffMs: v.number(), - }), - ), - variables: v.optional(v.record(v.string(), v.any())), // default variables - }), - ), - - metadata: v.optional(v.any()), -}) - .index('by_org', ['organizationId']) - .index('by_org_status', ['organizationId', 'status']); -``` - -### wfStepDefs (step definitions) - -```typescript -wfStepDefs: defineTable({ - organizationId: v.string(), // Better Auth organization ID - wfDefinitionId: v.id('wfDefinitions'), - - stepSlug: v.string(), // unique step identifier within workflow - name: v.string(), - description: v.optional(v.string()), - stepType: v.union( - v.literal('trigger'), - v.literal('llm'), - v.literal('condition'), - v.literal('approval'), - v.literal('action'), - ), - order: v.number(), // execution order - - // Flow control - structured next step definitions - nextSteps: v.object({ - default: v.optional(v.string()), - onSuccess: v.optional(v.string()), - onApprove: v.optional(v.string()), - onReject: v.optional(v.string()), - onTrue: v.optional(v.string()), - onFalse: v.optional(v.string()), - }), - - // Step-specific configuration - config: v.any(), // varies by stepType - - // Input/output mapping - inputMapping: v.optional(v.record(v.string(), v.string())), - outputMapping: v.optional(v.record(v.string(), v.string())), - - metadata: v.optional(v.any()), -}) - .index('by_definition', ['wfDefinitionId']) - .index('by_definition_order', ['wfDefinitionId', 'order']) - .index('by_step_slug', ['wfDefinitionId', 'stepSlug']); -``` - -### wfExecutions (single source of truth for state) - -```typescript -wfExecutions: defineTable({ - organizationId: v.string(), // Better Auth organization ID - wfDefinitionId: v.id('wfDefinitions'), // references workflow template - status: v.string(), // 'pending' | 'running' | 'completed' | 'failed' - currentStepSlug: v.string(), // current step being executed - waitingFor: v.optional(v.string()), // 'approval' | null - startedAt: v.number(), - updatedAt: v.number(), - completedAt: v.optional(v.number()), - - // Link to component workflow runtime (get-convex/workflow) - componentWorkflowId: v.optional(v.string()), - - // Execution context and variables - variables: v.optional(v.any()), // runtime variables - input: v.optional(v.any()), // initial input data - output: v.optional(v.any()), // final output data - - // Trigger information - triggeredBy: v.optional(v.string()), // 'manual' | 'schedule' | 'webhook' | 'event' - triggerData: v.optional(v.any()), // trigger-specific data - - metadata: v.optional(v.any()), -}) - .index('by_org', ['organizationId']) - .index('by_definition', ['wfDefinitionId']) - .index('by_status', ['status']) - .index('by_org_status', ['organizationId', 'status']) - .index('by_component_workflow', ['componentWorkflowId']); -``` - -### approvals (unified approval system) - -```typescript -approvals: defineTable({ - organizationId: v.string(), // Better Auth organization ID - workflowExecutionId: v.optional(v.id('wfExecutions')), // references execution - stepSlug: v.optional(v.string()), // references the approval step - approverMemberId: v.optional(v.id('members')), // can be null for unassigned tasks - status: v.string(), // 'pending' | 'approved' | 'rejected' - - // Approval data - submittedData: v.any(), // data submitted for approval - decision: v.optional(v.string()), // 'approve' | 'reject' - comments: v.optional(v.string()), - reviewedAt: v.optional(v.number()), - decidedAt: v.optional(v.number()), - assignedAt: v.optional(v.number()), - - // UI routing and resource hints - resourceType: v.union( - v.literal('conversations'), - v.literal('product_recommendation'), - ), // 'conversations' | 'product_recommendation' - resourceId: v.string(), - routeHint: v.optional(v.string()), // 'conversation' | 'task_center' - deeplink: v.optional(v.string()), - - // Priority and timing - priority: v.string(), // 'low' | 'medium' | 'high' | 'urgent' - dueDate: v.optional(v.number()), - - metadata: v.optional(v.any()), -}) - .index('by_organization', ['organizationId']) - .index('by_approver_status', ['approverMemberId', 'status']) - .index('by_execution', ['workflowExecutionId']) - .index('by_org_status', ['organizationId', 'status']) - .index('by_resource', ['resourceType', 'resourceId']); -``` - -## 4) Workflow definition examples - -### Simple approval workflow (replaces traditional tasks) - -```typescript -// Workflow template -{ - name: "Product Recommendation Approval", - config: { - timeout: 86400000, // 24 hours - variables: { - defaultPriority: "medium" - } - } -} - -// Workflow steps -[ - { - stepSlug: "start", - stepType: "trigger", - order: 1, - config: { - type: "manual" - }, - nextSteps: { default: "approval" } - }, - { - stepSlug: "approval", - stepType: "approval", - order: 2, - config: { - approverRole: "reviewer", - resourceType: "product_recommendation", - allowDataModification: false - }, - nextSteps: { - onApprove: "send_recommendation", - onReject: "log_rejection" - } - }, - { - stepSlug: "send_recommendation", - stepType: "action", - order: 3, - config: { - type: "conversation", - parameters: { - operation: "create", - // Create an email conversation with the approved recommendations - organizationId: "{{organizationId}}", - customerId: "{{customerId}}", - subject: "Product Recommendations", - channel: "email", - direction: "outbound" - } - }, - nextSteps: { default: "end" } - }, - { - stepSlug: "log_rejection", - stepType: "action", - order: 4, - config: { - type: "log", - message: "Product recommendation rejected" - }, - nextSteps: { default: "end" } - } -] -``` - -### Data synchronization workflow - -```typescript -// Workflow template -{ - name: 'Shopify Data Sync'; -} - -// Workflow steps -[ - { - stepSlug: 'start', - stepType: 'trigger', - order: 1, - config: { - type: 'schedule', - schedule: '0 */6 * * *', // every 6 hours - timezone: 'UTC', - }, - nextSteps: { default: 'sync_products' }, - }, - { - stepSlug: 'sync_products', - stepType: 'action', - order: 2, - config: { - type: 'shopify_sync', - resource: 'products', - batchSize: 100, - }, - nextSteps: { - onSuccess: 'sync_customers', - }, - }, - { - stepSlug: 'sync_customers', - stepType: 'action', - order: 3, - config: { - type: 'shopify_sync', - resource: 'customers', - batchSize: 50, - }, - nextSteps: { - onSuccess: 'complete', - }, - }, -]; -``` - -### Complex business process workflow - -```typescript -// Workflow template -{ - name: "Customer Churn Prevention", - triggers: { - event: "subscription_cancelled" - } -} - -// Workflow steps -[ - { - stepSlug: "analyze_churn_risk", - stepType: "llm", - order: 1, - config: { - name: "Analyze Churn Risk", - // Model is configured globally via OPENAI_MODEL and is not set per step - temperature: 0.2, - systemPrompt: "Analyze customer churn risk based on: {{customer_data}}" - }, - inputMapping: { - customer_data: "{{workflow.input.customer}}" - }, - nextSteps: { default: "risk_assessment" } - }, - { - stepSlug: "risk_assessment", - stepType: "condition", - order: 2, - config: { - expr: "{{churn_risk_score}} > 0.7" - }, - nextSteps: { - onTrue: "manual_review", - onFalse: "auto_retention" - } - }, - { - stepSlug: "manual_review", - stepType: "approval", - order: 3, - config: { - approverRole: "account_manager", - resourceType: "churn_prevention", - priority: "high" - }, - nextSteps: { - onApprove: "personal_outreach", - onReject: "auto_retention" - } - }, - { - stepSlug: "auto_retention", - stepType: "action", - order: 4, - config: { - type: "send_retention_email", - template: "standard_retention" - } - } -] -``` - -## 5) Supported node types (MVP) - -### Common step metadata - -- stepSlug: string (unique within workflow) -- stepType: 'trigger' | 'llm' | 'condition' | 'approval' | 'action' -- order: number -- nextSteps: structured flow control object -- inputMapping: variable mapping from workflow context -- outputMapping: variable mapping to workflow context - -### 1. Trigger - -- Purpose: Start workflow execution -- Config: varies by trigger type -- Behavior: initializes execution context - -```typescript -{ - stepSlug: 'start', - stepType: 'trigger', - order: 1, - config: { - type: 'manual' | 'schedule' | 'webhook' | 'event' - }, - nextSteps: { default: 'next_step' } -} -``` - -### 2. LLM - -- Purpose: AI reasoning/generation -- Config: model settings and prompts -- Inputs: prompt, context data -- Outputs: AI response, usage metrics - -```typescript -{ - stepSlug: 'analyze', - stepType: 'llm', - order: 2, - config: { - name: 'Analyzer', - // Model is configured globally via OPENAI_MODEL and is not set per step - temperature: 0.2, - maxTokens: 1000, - systemPrompt: 'Analyze the following data: {{input_data}}' - }, - nextSteps: { default: 'next_step' } -} -``` - -### 3. Condition - -- Purpose: Boolean branching logic -- Config: expression to evaluate -- Inputs: variables referenced in expression -- Outputs: boolean result - -```typescript -{ - stepSlug: 'gate', - stepType: 'condition', - order: 3, - config: { - expr: '{{score}} > 0.7' - }, - nextSteps: { - onTrue: 'approve_path', - onFalse: 'reject_path' - } -} -``` - -### 4. Approval - -- Purpose: Human-in-the-loop decision making -- Config: approver settings and UI hints -- Side effects: creates Approval record and marks execution as waiting (`waitingFor='approval'`) -- Resume: via approve/reject actions - -```typescript -{ - stepSlug: 'review', - stepType: 'approval', - order: 4, - config: { - approverMemberId: 'member_123', - resourceType: 'product_recommendation', - priority: 'high', - dueDate: '{{now + 86400000}}' // 24 hours - }, - nextSteps: { - onApprove: 'send_recommendation', - onReject: 'log_rejection' - } -} -``` - -### 5. Action - -- Purpose: Execute side effects -- Config: action type and parameters -- Types: log, webhook, email, platform_integration, etc. - -```typescript -{ - stepSlug: 'notify', - stepType: 'action', - order: 5, - config: { - type: 'webhook', - url: 'https://api.example.com/notify', - method: 'POST', - body: { - event: 'workflow_completed', - data: '{{workflow.output}}' - } - }, - nextSteps: { default: 'end' } -} -``` - -### Validation (minimal) - -- stepSlug: required, unique within workflow -- stepType: required, must be one of supported types -- routeability: all nextSteps targets must refer to existing stepSlug (or end) -- approval: approverMemberId required -- condition: expr required; must resolve to boolean - -## 6) Scheduled triggers (cron) - -- Store cron expressions in workflow.triggers.schedule -- Use Convex crons to scan and trigger eligible workflows - -```typescript -const crons = cronJobs(); -crons.cron( - 'scan workflows', - '*/5 * * * *', - internal.workflows.scanAndTrigger, - {}, -); -export default crons; -``` - -- Internal action scans active workflows with due schedules - -```typescript -export const scanAndTrigger = internalAction({ - args: {}, - returns: v.null(), - handler: async (ctx) => { - // Find workflows with schedule triggers that are due - // Create new executions for each eligible workflow - return null; - }, -}); -``` - -## 7) Execution lifecycle (pause/resume) - -### Start - -- Create wfExecutions(status='running') -- Initialize variables with input data and workflow defaults -- Log workflow_started event - -### Step Processing - -- Load current step definition from wfStepDefs -- Execute step based on stepType -- Update execution variables with step outputs -- Determine next step based on step result and nextSteps configuration - -### Approval Step (pause/resume) - -- Create approvals(status='pending') -- Update wfExecutions -> status remains 'running', waitingFor='approval' -- Optionally log a workflow_waiting event -- On approve/reject: update approvals and resume execution -- Continue to next step based on approval decision - -### Completion - -- Update workflowExecutions -> status='completed'/'failed' -- Set completedAt timestamp -- Log workflow_completed/workflow_failed event - -- Cancel behavior: Cancelling an execution sets status='failed' with error='canceled'. - -## 8) Unified work management - -### Replacing traditional tasks - -All work items are now modeled as workflows: - -1. **Simple approval tasks** → Single-step approval workflows -2. **Data sync tasks** → Single-step action workflows -3. **Complex processes** → Multi-step workflows with conditions and approvals -4. **Recurring work** → Scheduled workflows - -### Benefits - -- **Unified interface**: All work managed through workflow system -- **Consistent state management**: Single execution model -- **Easy evolution**: Simple workflows can grow into complex processes -- **Better visibility**: All work visible in workflow dashboard -- **Audit trail**: Complete execution history in logs - -### Migration from tasks - -1. **Product recommendation approvals** → Approval workflows -2. **Data synchronization** → Action workflows -3. **Manual assignments** → Manual trigger workflows -4. **Recurring maintenance** → Scheduled workflows - -## 9) Minimal API surface (Convex new syntax) - -### Start/Run workflow - -```typescript -export const run = mutation({ - args: { - wfDefinitionId: v.id('wfDefinitions'), - input: v.optional(v.any()), - triggeredBy: v.optional(v.string()), - }, - returns: v.object({ executionId: v.id('wfExecutions') }), - handler: async (ctx, args) => { - // Create execution and start workflow - }, -}); -``` - -### Step runner (internal) - -```typescript -export const processStep = internalMutation({ - args: { executionId: v.id('wfExecutions') }, - returns: v.null(), - handler: async (ctx, args) => { - // Load current step and execute based on stepType - return null; - }, -}); -``` - -### Approve / Reject - -```typescript -export const approve = mutation({ - args: { - approvalId: v.id('approvals'), - decision: v.union(v.literal('approve'), v.literal('reject')), - comments: v.optional(v.string()), - }, - returns: v.null(), - handler: async (ctx, args) => { - // Update approval record and resume workflow - }, -}); -``` - -## 10) State machine (execution.status) - -- **pending** → **running** → **completed** -- **running** → **failed** (on unrecoverable error or cancel; cancel records error='canceled') -- currentStepId indicates progress -- waitingFor explains pause reason ('approval' | null) -- Approval records can be found by querying the approvals table with workflowExecutionId - -## 11) Human approval and UI integration - -### Unified approval system - -- All approvals flow through Approvals table -- Single "My Tasks" inbox for all approval types -- Consistent approval interface across different resource types - -### Resource type routing - -- **product_recommendation** → Task center with product details -- **message_draft** → Conversation UI with editing capabilities -- **predefined_workflow** → Admin dashboard with sync status -- **churn_prevention** → Customer management interface - -### Approval workflow - -1. Workflow reaches approval step -2. Approval record created with resource metadata -3. Approver receives notification with deep link -4. Approver reviews in appropriate UI -5. Decision recorded, workflow resumes -6. Next step determined by approval outcome - -## 12) Example: End-to-end workflow execution - -### Product recommendation approval workflow - -1. **Trigger**: Customer makes purchase, triggers workflow -2. **Data preparation**: Load customer history and product catalog -3. **AI analysis**: Generate product recommendations using LLM -4. **Quality check**: Condition step validates recommendation quality -5. **Human approval**: If quality uncertain, pause for manual review (status stays running, `waitingFor='approval'`) -6. **Action**: Send approved recommendations or log rejection -7. **Completion**: Update customer profile and log results - -### Execution flow - -``` -purchase_event → load_data → generate_recommendations → quality_check - ↓ - [high_quality] - ↓ - send_recommendations - ↓ - complete - - [low_quality] - ↓ - manual_review - ↓ ↓ - [approve] [reject] - ↓ ↓ - send_recommendations log_rejection - ↓ ↓ - complete complete -``` - -### Example: approval pause/resume (core snippet) - -```typescript -const approvalId = await ctx.db.insert('approvals', { - organizationId, - workflowExecutionId: execId, - stepSlug: currentStepSlug, - approverMemberId, - status: 'pending', - submittedData: stepOutput, - resourceType: 'product_recommendation', - resourceId: 'some_resource_id', - priority: 'medium', - assignedAt: Date.now(), -}); - -await ctx.db.patch(execId, { - waitingFor: 'approval', - updatedAt: Date.now(), -}); - -This unified workflow system replaces the traditional task management approach with a more flexible, scalable, and consistent model that can handle everything from simple approvals to complex business processes. -``` diff --git a/docs/workflows/auto-processing-pattern.md b/docs/workflows/auto-processing-pattern.md deleted file mode 100644 index a13e72f4e0..0000000000 --- a/docs/workflows/auto-processing-pattern.md +++ /dev/null @@ -1,355 +0,0 @@ -# Auto-Processing Pattern for Agent Orchestration Workflows - -## Overview - -This pattern enables workflows to automatically find and process entities (customers, products, etc.) one at a time, with intelligent termination when no work is needed. - -## Key Concepts - -### 1. Workflow-Specific Processing Tracking - -Each workflow tracks its own processing history independently using `workflowId`: - -```typescript -// Customer metadata structure -{ - metadata: { - workflows: { - "assess-customer-status": { - lastProcessedAt: "2024-01-15T10:30:00Z", - lastExecutionId: "exec_123", - processCount: 5 - }, - "send-churn-survey": { - lastProcessedAt: "2024-01-10T08:00:00Z", - lastExecutionId: "exec_456", - processCount: 2 - } - } - } -} -``` - -**Benefits:** - -- ✅ Same customer can be processed by different workflows independently -- ✅ Each workflow has its own processing schedule -- ✅ No conflicts between different workflow types -- ✅ Clear audit trail per workflow - -### 2. One Entity Per Execution - -Each workflow execution processes exactly ONE entity: - -- **Scheduled trigger** runs periodically (e.g., every 5 minutes) -- **Finder Agent** searches for ONE unprocessed entity -- **Processing Agents** analyze and update that entity -- **Update Agent** marks entity as processed - -**Benefits:** - -- ✅ Simple, predictable execution -- ✅ Easy to debug and monitor -- ✅ Natural rate limiting -- ✅ Clear conversation context (one entity per thread) - -### 3. Intelligent Workflow Termination - -Workflows can terminate early when no work is needed: - -```typescript -// Termination signal from Finder Agent -{ - "shouldTerminate": true, - "reason": "No customers found that haven't been processed in the last 3 days", - "terminationType": "NO_DATA_FOUND", - "metadata": { - "workflowId": "assess-customer-status", - "daysBack": 3, - "searchTimestamp": "2024-01-15T10:30:00Z" - } -} -``` - -**Benefits:** - -- ✅ No wasted processing when no work is needed -- ✅ Clear audit trail of why workflow terminated -- ✅ AI-driven decision making -- ✅ Automatic resource optimization - -## Workflow Structure - -### Standard Pattern - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. Scheduled Trigger (every N minutes) │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 2. Finder Agent │ -│ - Search for ONE unprocessed entity │ -│ - Check: metadata.workflows.{workflowId}.lastProcessedAt │ -│ - If found → Return entity data │ -│ - If NOT found → Return termination signal │ -└────────────┬───────────────────────────────┬────────────────┘ - │ │ - │ (entity found) │ (no entity) - ▼ ▼ -┌────────────────────────────┐ ┌─────────────────────────┐ -│ 3. Analyzer Agent │ │ 5. Finish │ -│ - Analyze entity │ │ (workflow terminated)│ -│ - Determine action │ └─────────────────────────┘ -└────────────┬───────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 4. Update Agent │ -│ - Perform updates │ -│ - Mark as processed: │ -│ metadata.workflows.{workflowId}.lastProcessedAt = now │ -└────────────┬────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 5. Finish │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Implementation Example - -### Workflow Configuration - -```typescript -export default { - workflowConfig: { - name: 'Single Customer Status Assessment', - description: - 'Automatically find and analyze one customer that needs status assessment', - workflowType: 'agent_orchestration' as const, - config: { - timeout: 120000, - retryPolicy: { maxRetries: 2, backoffMs: 1000 }, - variables: { - organizationId: 'org_demo', - workflowId: 'assess-customer-status', // Used for tracking - daysBack: 3, // Process customers not processed in last 3 days - }, - }, - }, - stepsConfig: [ - // Step 1: Scheduled Trigger - { - stepSlug: 'start', - stepType: 'trigger', - config: { - type: 'scheduled', - schedule: '*/5 * * * *', // Every 5 minutes - }, - nextSteps: { default: 'customer_finder_agent' }, - }, - - // Step 2: Finder Agent - { - stepSlug: 'customer_finder_agent', - stepType: 'llm', - config: { - llmNode: { - systemPrompt: `Find ONE customer not processed by {{workflowId}} in last {{daysBack}} days. - -If no customer found, return termination signal: -{ - "shouldTerminate": true, - "reason": "...", - "terminationType": "NO_DATA_FOUND" -}`, - tools: ['customer_search'], - }, - }, - nextSteps: { - success: 'status_analyzer_agent', - terminate: 'finish', // Auto-route on termination - }, - }, - - // Step 3: Analyzer Agent - { - stepSlug: 'status_analyzer_agent', - stepType: 'llm', - config: { - llmNode: { - systemPrompt: 'Analyze customer status...', - tools: [], - }, - }, - nextSteps: { success: 'update_executor_agent' }, - }, - - // Step 4: Update Agent - { - stepSlug: 'update_executor_agent', - stepType: 'llm', - config: { - llmNode: { - systemPrompt: `Update customer and mark as processed: -- metadata.workflows.{{workflowId}}.lastProcessedAt = now -- metadata.workflows.{{workflowId}}.lastExecutionId = {{executionId}}`, - tools: ['customer_update'], - }, - }, - nextSteps: { success: 'finish' }, - }, - - // Step 5: Finish - { - stepSlug: 'finish', - stepType: 'trigger', - config: { type: 'manual' }, - nextSteps: {}, - }, - ], -}; -``` - -## Finder Agent Prompt Template - -```typescript -systemPrompt: `You are a Finder Agent. Find ONE entity that needs processing. - -WORKFLOW TERMINATION PROTOCOL: -If NO entities need processing, return: -{ - "shouldTerminate": true, - "reason": "No entities found that haven't been processed by workflow '{{workflowId}}' in the last {{daysBack}} days", - "terminationType": "NO_DATA_FOUND", - "metadata": { - "workflowId": "{{workflowId}}", - "daysBack": {{daysBack}}, - "searchTimestamp": "ISO date" - } -} - -Search criteria: -1. Find entities where metadata.workflows.{{workflowId}}.lastProcessedAt is: - - Missing (never processed), OR - - Older than {{daysBack}} days ago -2. Return ONLY ONE entity (first eligible) -3. If none found, return termination signal - -SUCCESS Response: -{ - "entityId": "...", - "entityData": {...}, - "lastProcessedAt": "ISO date or null", - "neverProcessed": boolean -}`, - -userPrompt: `Find ONE entity for workflow: {{workflowId}} -Days back: {{daysBack}} -Metadata path: workflows.{{workflowId}}.lastProcessedAt` -``` - -## Update Agent Prompt Template - -```typescript -systemPrompt: `You are an Update Agent. Update entity and mark as processed. - -CRITICAL: After updating, mark as processed: -- metadata.workflows.{{workflowId}}.lastProcessedAt = current ISO timestamp -- metadata.workflows.{{workflowId}}.lastExecutionId = "{{executionId}}" - -This prevents duplicate processing within the time window.`, - -userPrompt: `Update the entity and mark as processed by workflow: {{workflowId}} -Execution ID: {{executionId}}` -``` - -## Benefits of This Pattern - -### 1. Scalability - -- Processes entities gradually over time -- Natural rate limiting (one per execution) -- No overwhelming database queries - -### 2. Reliability - -- Clear processing state per workflow -- No duplicate processing within time window -- Easy to retry failed executions - -### 3. Observability - -- Each execution has clear audit trail -- Easy to see which entities were processed when -- Termination reasons are logged - -### 4. Flexibility - -- Different workflows can process same entities independently -- Each workflow has its own schedule and rules -- Easy to adjust processing frequency - -### 5. Resource Efficiency - -- Auto-terminates when no work needed -- No wasted processing cycles -- Optimal use of AI agent resources - -## Common Use Cases - -1. **Customer Status Assessment** - - - Find customers needing status update - - Analyze subscription patterns - - Update status and mark as processed - -2. **Churn Prevention** - - - Find at-risk customers - - Generate personalized retention offers - - Track outreach attempts - -3. **Product Recommendations** - - - Find customers eligible for recommendations - - Generate AI-powered suggestions - - Track recommendation history - -4. **Data Quality Checks** - - Find entities with incomplete data - - Validate and enrich data - - Mark as validated - -## Best Practices - -1. **Use Descriptive Workflow IDs** - - - Use kebab-case: `assess-customer-status` - - Make it clear what the workflow does - - Keep it unique across all workflows - -2. **Set Appropriate Time Windows** - - - Too short: Excessive processing - - Too long: Stale data - - Typical: 1-7 days depending on use case - -3. **Handle Termination Gracefully** - - - Always provide clear termination reasons - - Include search metadata for debugging - - Log termination events - -4. **Track Processing Metadata** - - - Always update `lastProcessedAt` - - Include `lastExecutionId` for audit trail - - Optionally track `processCount` - -5. **Test Termination Logic** - - Verify termination signal is detected - - Ensure workflow ends gracefully - - Check audit logs are complete diff --git a/docs/workflows/data-spec.md b/docs/workflows/data-spec.md deleted file mode 100644 index 594c7a7018..0000000000 --- a/docs/workflows/data-spec.md +++ /dev/null @@ -1,516 +0,0 @@ -# Workflow Data Model & Conventions - -This document defines the canonical data model and conventions used by the workflow engine. It explains the structure of variables available during execution, the shape of step outputs, and how loop processing interoperates with the rest of the system. - -The goals of this specification: - -- Clear, predictable variable names and scopes -- Minimal, consistent contracts between nodes -- Easy referencing from templates and expressions - ---- - -## 1) Global Variables Object - -During execution, the engine maintains a single variables object stored in `wfExecutions.variables` with a small, fixed set of root keys: - -- `steps`: Record of per-step outputs and metadata - - Type: `Record` - - Access pattern: `steps[stepSlug].output` - - Each step entry includes: `stepType`, `name`, and `output` -- `lastOutput`: The most recent step's output - - Type: `StepOutput | null` - - Access pattern: `lastOutput` - - Updated after each step completes -- `loop`: Loop execution namespace (only written by Loop node) - - Type: `LoopVars` - - Contains: `items`, `state`, `item`, `index` (sanitized to avoid persisting huge arrays) -- `organizationId`: Organization identifier injected by the system - - Type: `Id<'organizations'>` -- `input`: Optional inbound payload (from workflow trigger) - - Type: `unknown` -- `secrets`: Optional secrets map (decrypted from workflowConfig.secrets) - - Type: `Record` - -Additional variables from `workflowConfig.variables` are merged at initialization. - -No other root-level keys should be added by step executors. - -### Example shape - -```ts -interface Variables { - steps: Record< - string, - { - stepType: string; - name: string; - output: StepOutput; - } - >; - lastOutput: StepOutput | null; - loop?: LoopVars; - organizationId: Id<'organizations'>; - input?: unknown; - secrets?: Record; - // Additional variables from workflowConfig.variables - [key: string]: unknown; -} -``` - ---- - -## 2) Step Output Envelope (StepOutput) - -Every node returns a standardized envelope called `StepOutput`: - -```ts -type StepOutput = { - type: string; // e.g. 'trigger' | 'llm' | 'condition' | 'action' | 'approval' | 'loop' | ... - data: unknown; // primary, small-to-medium result payload - meta?: Record; // optional, for rich context/diagnostics -}; -``` - -Guidelines: - -- Put the primary result in `data`. -- Place additional context (diagnostics, traces, tool info, etc.) in `meta`. -- Keep both `data` and `meta` reasonably small. - -The engine persists each step's `StepOutput` into `steps[stepSlug].output` and sets `lastOutput` to the same `StepOutput`. - ---- - -## 3) Node Result Contract - -Each node returns a `StepExecutionResult` containing routing and the output envelope: - -```ts -interface StepExecutionResult { - port: string; // routing key - output: StepOutput; - variables?: { loop?: LoopVars }; // Only Loop node writes here - error?: string; - threadId?: string; - approvalTaskId?: string; -} -``` - -**Note**: If a step fails, it throws an exception instead of returning a failure port. The workflow execution will be marked as failed. - -Rules: - -- `variables` may only contain the `loop` namespace. Non-loop nodes should omit `variables`. -- `port` is used to select the next step via the current step's `nextSteps` mapping. - -Common `port` values by node type: - -- trigger: `success` -- condition: `true` | `false` -- action: `success` (failures throw exceptions) -- approval: `approved` | `rejected` (when resumed after approval decision) -- llm: `success` -- loop: `loop` | `done` - -**Note on Approval Waiting:** -When an approval step is first executed, it creates an approval record and marks the execution as waiting (`waitingFor='approval'`) while keeping `status='running'`. -The workflow pauses and waits for user action. When the user approves/rejects via the API: - -1. `approveWorkflowStep` or `rejectWorkflowStep` updates the approval status -2. `processApprovalResponse` is scheduled to resume the workflow -3. The workflow resumes with `resumeWithPort` set to `'approved'` or `'rejected'` -4. The manager routes to the next step based on this port - ---- - -## 4) Loop Namespace (LoopVars) - -Loop state is isolated under `variables.loop` and only written by the Loop node. - -```ts -interface LoopVars { - items?: unknown[]; // full items collection used by the loop - state?: { - currentIndex: number; // next index to process - totalItems: number; - iterations: number; // how many loop ticks have occurred - batchesProcessed: number; // number of batches processed - isComplete: boolean; - }; - batch?: { - index: number; // batch index (0-based) - size: number; // batch size for this tick - items: unknown[]; // items for this batch - startIndex: number; // inclusive - endIndex: number; // inclusive - } | null; - - // Convenience fields when batchSize === 1 - item?: unknown; // current single item - index?: number; // current item index -} -``` - -Loop behavior: - -- First tick initializes `state` and (if non-empty) sets `batch` to the first slice of `items`. -- Subsequent ticks advance `currentIndex`, update `batch` accordingly, and increment `iterations` / `batchesProcessed`. -- Completion sets `state.isComplete = true` and `batch = null`, and returns `port = 'done'`. - -The Loop node places its result in: - -- `variables.loop` (items/state/batch/[item/index]) -- `output`: `{ type: 'loop', data: { state, batch } }` - ---- - -## 5) Access Patterns (Templates / Expressions) - -- Last step output: - - `{{ lastOutput.data }}...` - - `{{ lastOutput.meta.someKey }}` -- Specific step output: - - `{{ steps.myStepSlug.output.data }}...` - - `{{ steps['myStepSlug'].output.meta.info }}` -- Loop context: - - `{{ loop.state.currentIndex }}` - - `{{ loop.batch.items }}` - - `{{ loop.item }}` and `{{ loop.index }}` (when `batchSize = 1`) - -Notes: - -- `lastOutput` and `steps[...].output` are always `StepOutput` envelopes; use `.data` for primary values. -- Avoid reaching into other namespaces; only `loop` is writable and only by the Loop node. - ---- - -## 6) Manager Persistence Rules - -After each step completes successfully, the engine persists a normalized variables snapshot to `wfExecutions.variables`: - -- Set `steps[stepSlug] = { stepType, name, output: result.output }` -- Set `lastOutput = result.output` -- If provided: set `loop = essentialLoop` (extracted to keep only `state`, `item`, `index`, `items`) -- Always keep `organizationId` intact -- Preserve all other existing variables from previous steps - -The persistence happens in `stepExecutor.ts` after each step execution via `updateExecutionVariables`. - -**Loop Variable Extraction:** -Loop variables are extracted before persistence to avoid storing huge arrays: - -```ts -const essentialLoop = { - state: loopVars.state, - item: loopVars.item, - index: loopVars.index, - items: loopVars.items, // Full items array kept for continuation -}; -``` - -No other root keys are added or modified by the step executor. - ---- - -## 7) Next-Step Routing - -Each step definition contains a port map (record of `port -> nextStepSlug`). The engine routes to the next step using the `port` returned by the current node: - -```ts -const map = stepDef.nextSteps || {}; -const nextStepSlug = map[stepResult.port] ?? null; -``` - -**Special Loop Routing:** -When a loop step returns `port = 'loop'`, the workflow manager routes back to the same loop step to process the next batch: - -```ts -if (stepDef.stepType === 'loop' && stepResult.port === 'loop') { - currentStepSlug = stepDef.stepSlug; // Loop back to same step -} -``` - -If `nextStepSlug` is `null`, the workflow ends and is marked as completed. - ---- - -## 8) Types Summary - -```ts -type StepOutput = { - type: string; - data: unknown; - meta?: Record; -}; - -interface StepExecutionResult { - port: string; - output: StepOutput; - variables?: { loop?: LoopVars }; // Only Loop node writes here - error?: string; - threadId?: string; - approvalTaskId?: string; -} - -interface Variables { - steps: Record< - string, - { - stepType: string; - name: string; - output: StepOutput; - } - >; - lastOutput: StepOutput | null; - loop?: LoopVars; - organizationId: Id<'organizations'>; - input?: unknown; - secrets?: Record; - [key: string]: unknown; // Additional variables from workflowConfig -} - -interface LoopVars { - items?: unknown[]; - state?: { - currentIndex: number; - totalItems: number; - iterations: number; - batchesProcessed: number; - isComplete: boolean; - }; - batch?: { - index: number; - size: number; - items: unknown[]; - startIndex: number; - endIndex: number; - } | null; - item?: unknown; - index?: number; -} -``` - ---- - -## 9) Best Practices - -- Keep `output.data` as the primary, succinct payload; place verbose context in `output.meta` when necessary. -- Only the Loop node writes `variables.loop`. Other nodes should not write to `variables`. -- Use `steps[stepSlug].output.data` and `lastOutput.data` consistently in templates for clarity and stability. -- Prefer adding semantic details to `output.meta` instead of introducing new variable namespaces. -- Step executors should NOT directly modify `wfExecutions.variables` - this is managed by `stepExecutor.ts`. -- Loop variables are automatically sanitized before persistence to avoid storing huge arrays. - ---- - -## 10) Execution Architecture - -### Database Schema - -**wfDefinitions** - Workflow templates - -- `organizationId`, `name`, `description`, `version`, `status` -- `config`: Contains `timeout`, `retryPolicy`, `variables`, `secrets` -- Indexes: `by_org`, `by_org_status` - -**wfStepDefs** - Step definitions for workflows - -- `wfDefinitionId`, `stepSlug`, `name`, `stepType`, `order` -- `config`: Step-specific configuration -- `nextSteps`: Port mapping (e.g., `{ success: 'step2' }`, `{ true: 'step3', false: 'step4' }`) -- Indexes: `by_definition`, `by_definition_order`, `by_step_id` - -**wfExecutions** - Workflow execution instances - -- `organizationId`, `wfDefinitionId`, `status`, `currentStepSlug` -- `variables`: Runtime variables (persisted after each step) -- `workflowConfig`: Workflow-level config (stored at execution time) -- `stepsConfig`: Map of stepSlug -> config (stored at execution time for quick access) -- `input`, `output`, `triggeredBy`, `triggerData` -- `componentWorkflowId`: Reference to @convex-dev/workflow component -- Indexes: `by_org`, `by_definition`, `by_status`, `by_component_workflow` - -**approvals** - Approval tasks - -- `organizationId`, `wfExecutionId`, `stepSlug`, `approverMemberId`, `status` -- `submittedData`, `decision`, `comments`, `reviewedAt` -- `resourceType`, `resourceId`, `routeHint`, `deeplink` -- `priority`, `dueDate` -- Indexes: `by_organization`, `by_approver_status`, `by_execution`, `by_resource` - -### Execution Flow - -1. **Workflow Start** (`api/execution.startWorkflow`) - - - Creates `wfExecution` record with status `running` - - Loads workflow definition and steps from database - - Stores `workflowConfig` and `stepsConfig` in execution record - - Delegates to `@convex-dev/workflow` component via `adapter/component.startWithComponent` - -2. **Component Workflow** (`manager.ts` - `dynamicWorkflow`) - - - Iterates through steps using `while (currentStepSlug)` loop - - Calls `stepExecutor.executeStep` for each step - - Handles loop routing (loops back to same step when `port = 'loop'`) - - Marks execution as completed/failed when done - -3. **Step Execution** (`core/stepExecutor.ts`) - - - Loads execution to get `stepsConfig` and `workflowConfig` - - Initializes variables on first step (merges input, workflowConfig.variables, decrypts secrets) - - Loads current variables from `wfExecutions.variables` - - Processes config with variable replacement - - Delegates to node-specific executor (trigger, llm, condition, approval, action, loop) - - Persists updated variables to `wfExecutions.variables` after step completes - - Returns control info: `{ success, port, error }` - -4. **Node Executors** (`nodes/*/executor.ts`) - - - Receive: `stepDef`, `variables`, `executionId` - - Execute node-specific logic - - Return: `StepExecutionResult` with `output`, `port`, optional `variables.loop` - -5. **Approval Flow** (`nodes/approval/executor.ts`) - - Creates approval task in `approvals` table - - Marks execution as waiting (`waitingFor='approval'`) while keeping `status='running'` - - When approved/rejected, `processApprovalResponse` resumes workflow - - Updates execution metadata with `resumeWithPort` hint - - Calls `workflowManager.resume()` to continue execution - -### Variable Initialization - -On first step execution, variables are initialized as: - -```ts -fullVariables = { - ...(resumeVariables ?? initialInput ?? {}), - ...(workflowConfig?.variables ?? {}), - organizationId: organizationId, - secrets: decryptedSecrets, // If workflowConfig.secrets exists -}; -``` - -### Variable Persistence - -After each step, variables are updated: - -```ts -merged = { - ...fullVariables, - lastOutput: result.output, - steps: { - ...existingSteps, - [stepSlug]: { stepType, name, output: result.output }, - }, - loop: essentialLoop, // If loop step - organizationId: organizationId, -}; -``` - ---- - -## 11) Minimal Examples - -Trigger node result (example): - -```ts -{ - success: true, - port: 'success', - output: { - type: 'trigger', - data: 'Trigger executed successfully', - meta: { trigger: { type: 'webhook', receivedAt: '...' } }, - } -} -``` - -LLM node result (example): - -```ts -{ - success: true, - port: 'success', - output: { - type: 'llm', - data: { answer: '...' }, - meta: { llm: { model: 'gpt-4o', temperature: 0.2 } }, - } -} -``` - -Loop node result (example tick): - -```ts -{ - success: true, - port: 'loop', // or 'done' when complete - variables: { - loop: { - items: [...], - state: { currentIndex: 10, totalItems: 100, iterations: 5, batchesProcessed: 5, isComplete: false }, - batch: { index: 5, size: 2, items: [...], startIndex: 10, endIndex: 11 }, - item: undefined, // present if batchSize === 1 - index: undefined // present if batchSize === 1 - } - }, - output: { - type: 'loop', - data: { state: { ... }, batch: { ... } } - } -} -``` - ---- - -## 12) Inline Workflows - -The system supports two execution modes: - -### Database Workflows - -- Workflow definition stored in `wfDefinitions` table -- Steps stored in `wfStepDefs` table -- Execution references `wfDefinitionId` -- Started via `api/execution.startWorkflow` - -### Inline Workflows - -- Workflow configuration passed directly to execution API -- No persistent workflow definition in database -- Execution has `wfDefinitionId: null` -- Started via `api/execution.executeWorkflowWithConfig` -- Metadata includes `isInlineExecution: true` - -Both modes use the same execution engine and flow through `@convex-dev/workflow` component. - -**Inline Workflow API:** - -```ts -executeWorkflowWithConfig({ - organizationId: Id<'organizations'>, - workflowConfig: { - name: string, - description?: string, - config?: { timeout?, retryPolicy?, variables?, secrets? } - }, - stepsConfig: [{ - stepSlug: string, - name: string, - stepType: 'trigger' | 'llm' | 'condition' | 'approval' | 'action' | 'loop', - order: number, - config: unknown, - nextSteps: Record - }], - input?: unknown, - triggeredBy: string, - triggerData?: unknown -}) -``` - -**Key Differences:** - -- Inline workflows store `workflowConfig` and `stepsConfig` in execution metadata -- Database workflows load config from `wfDefinitions` and `wfStepDefs` tables -- Both store `workflowConfig` and `stepsConfig` in `wfExecutions` for quick access during execution diff --git a/docs/workflows/database-operations.md b/docs/workflows/database-operations.md deleted file mode 100644 index 40b1c400f2..0000000000 --- a/docs/workflows/database-operations.md +++ /dev/null @@ -1,179 +0,0 @@ -# Workflow Database Operations - -This directory contains specialized, safe database operations for workflows. - -## Overview - -All database operations for workflows are organized by table/entity type. Each entity has its own folder with specialized operations that follow Convex best practices. - -## Available Operations - -### 1. Customer Operations (`customers/`) - -**Operations:** - -- `createCustomer` - Create a new customer -- `queryCustomers` - Query customers with flexible filters -- `getCustomerById` - Get a single customer by ID -- `updateCustomers` - Update customer fields and metadata - -**Features:** - -- Uses `by_organizationId` and `by_organizationId_and_externalId` indexes -- Supports status and metadata filtering -- Safe nested metadata updates with lodash -- Requires organizationId or customerId to prevent bulk operations - -**Documentation:** See `customers/README.md` - -### 2. Conversation Operations (`conversations/`) - -**Operations:** - -- `createConversation` - Create a new conversation -- `queryConversations` - Query conversations with flexible filters -- `getConversationById` - Get a single conversation by ID -- `updateConversations` - Update conversation fields and metadata - -**Features:** - -- Uses `by_organizationId` index -- Supports status, priority, customerId, and metadata filtering -- Safe nested metadata updates with lodash -- Requires organizationId or conversationId to prevent bulk operations - -### 3. Product Operations (`products/`) - -**Operations:** - -- `createProduct` - Create a new product -- `queryProducts` - Query products with flexible filters -- `getProductById` - Get a single product by ID -- `updateProducts` - Update product fields and metadata - -**Features:** - -- Uses `by_organizationId`, `by_organizationId_and_externalId`, `by_organizationId_and_status`, and `by_organizationId_and_category` indexes -- Supports status, category, externalId, and metadata filtering -- Safe nested metadata updates with lodash -- Requires organizationId or productId to prevent bulk operations - -### 4. Document Operations (`documents/`) - -**Operations:** - -- `createDocument` - Create a new document -- `queryDocuments` - Query documents with flexible filters -- `getDocumentById` - Get a single document by ID -- `updateDocument` - Update a single document's fields and metadata (requires `documentId`) - -**Features:** - -- Uses `by_organizationId` and `by_organizationId_and_kind` indexes for queries -- Supports kind and metadata filtering when querying -- Safe nested metadata updates with lodash (metadata objects are deep-merged with existing metadata) -- Requires `documentId` for updates to prevent bulk operations -- For batch updates, iterate over document IDs and call `updateDocument` for each document - -## Design Principles - -### 1. One Function Per File - -Each operation is in its own file with a matching name: - -- **File name:** `snake_case` (e.g., `query_customers.ts`) -- **Function name:** `camelCase` (e.g., `queryCustomers`) - -### 2. Use Convex Indexes - -All queries use Convex indexes (`withIndex`) instead of dynamic filters: - -```typescript -// ✅ Good: Uses index -const customers = await ctx.db - .query('customers') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - -// ❌ Bad: Dynamic filter (removed) -const customers = await ctx.db - .query('customers') - .filter((q) => q.eq(q.field('organizationId'), args.organizationId)) - .collect(); -``` - -### 3. Require Filters - -All operations require either: - -- A specific ID (customerId, productId, conversationId) -- An organizationId (to scope the query) - -This prevents accidental bulk operations on all records. - -### 4. Safe Nested Updates - -All update operations use lodash for safe nested object handling: - -```typescript -import { set, merge, get } from 'lodash'; - -// For dot-notation keys -set(metadata, 'churn.survey.sent', true); - -// For object merging (preserves existing fields) -merge(existingMetadata, newMetadata); -``` - -### 5. Filter in Code is OK - -Per [Convex Best Practices](https://docs.convex.dev/understanding/best-practices/): - -> "Filtering in code instead of using the .filter syntax has the same performance" - -We use `withIndex` for organizationId (efficient), then filter in code for metadata. This is acceptable because: - -1. We're already scoped to one organization (small result set) -2. Metadata fields are flexible and can't be indexed -3. The alternative would be a full table scan anyway - -## Adding New Operations - -To add operations for a new table: - -### Step 1: Create Database Operations - -1. Create a new folder: `convex/workflow/database/{table_name}/` -2. Create operation files following the naming convention -3. Create an `index.ts` to export all operations -4. Add exports to `executor.ts` - -### Step 2: Create Action Definition - -1. Create `convex/workflow/nodes/action/actions/{table_name}/{table_name}_action.ts` -2. Define the action with proper validators -3. Implement the `execute` method to call database operations -4. Register in `action_registry.ts` - -### Step 3: Update Workflows - -Replace `type: 'database'` with `type: '{table_name}'` in workflow examples. - -## Migration from Generic Database Actions - -The generic `database` action has been **completely removed** due to security issues: - -- ❌ Empty filter could affect all records (data loss risk) -- ❌ Dynamic filter parsing was error-prone -- ❌ Didn't follow Convex best practices - -All workflows have been migrated to use specialized actions. - -## References - -- [Convex Best Practices](https://docs.convex.dev/understanding/best-practices/) -- [Indexes and Query Performance](https://docs.convex.dev/database/reading-data/indexes/indexes-and-query-perf) -- [Lodash Documentation](https://lodash.com/docs/) -- [Migration Guide](../nodes/action/actions/MIGRATION.md) diff --git a/docs/workflows/developer-guide.md b/docs/workflows/developer-guide.md deleted file mode 100644 index e18f9283ef..0000000000 --- a/docs/workflows/developer-guide.md +++ /dev/null @@ -1,680 +0,0 @@ -## Workflow Module — Developer Guide - -This document describes the workflow system implementation, including the data model, public APIs, step/node execution, the adapter to the component engine, scheduling, logging, and example client usage. - -### Highlights - -- **Templates**: manage workflows via `wfDefinitions` and `wfStepDefs` -- **Execution**: track `wfExecutions`, approvals in `approvals` -- **Node types**: `trigger`, `llm`, `condition`, `approval`, `action`, `loop` -- **Adapter**: delegates execution to `@convex-dev/workflow` components with journaling -- **Scheduler**: scans active scheduled workflows every 5 minutes -- **🆕 Inline Execution**: execute workflows with manually passed configurations (bypasses database) -- **🆕 Unified Architecture**: single execution engine for both database and inline workflows - ---- - -## Architecture - -``` -convex/workflow/ -├── api/ # Public Convex functions (management, execution, scheduler) -├── adapter/ # Bridge to component engine (start/driver/completion) -├── core/ # Step executor entrypoint (internal) -├── database/ # Internal CRUD for definitions, steps, executions -├── manager.ts # WorkflowManager (dynamic workflow definition) -├── nodes/ # Node executors (trigger, llm, condition, approval, action, loop) -├── types/ # Type defs and validators -├── utils/ # Helpers (variables, conditions, helpers) -``` - -Key flows: - -- Client → `api/management` to manage templates and steps -- Client → `api/execution` to start/trigger, monitor, cancel/resume, read logs/stats -- `api/execution.startWorkflow` → `adapter/component.startWithComponent` -- Component engine journals steps → `adapter/component.componentDriver` → `runComponentDriver` -- Completion is mirrored back to `wfExecutions` via `onWorkflowComplete` - ---- - -## Data model (schema) - -Tables and notable indexes from `convex/schema.ts`: - -- `wfDefinitions` - - - Fields: `organizationId`, `name`, `description?`, `version`, `status`, `triggers`, `config?`, `metadata?` - - Indexes: `by_org`, `by_org_status` - -- `wfStepDefs` - - - Fields: `organizationId`, `wfDefinitionId`, `stepSlug`, `name`, `description?`, `stepType`, `order`, `nextSteps`, `config`, `inputMapping?`, `outputMapping?`, `metadata?` - - Indexes: `by_definition`, `by_definition_order`, `by_step_id` - -- `wfExecutions` - - - Fields: `organizationId`, `wfDefinitionId`, `status`, `currentStepId`, `waitingFor?`, `startedAt`, `updatedAt`, `completedAt?`, `componentWorkflowId?`, `variables?`, `input?`, `output?`, `triggeredBy?`, `triggerData?`, `metadata?` - - Indexes: `by_org`, `by_definition`, `by_status`, `by_org_status`, `by_component_workflow` - -- `approvals` - - Fields: `organizationId`, `wfExecutionId?`, `stepSlug?`, `approverMemberId?`, `status`, `submittedData`, `decision?`, `comments?`, `reviewedAt?`, `decidedAt?`, `assignedAt?`, `resourceType`, `resourceId`, `routeHint?`, `deeplink?`, `priority`, `dueDate?`, `metadata?` - - Indexes: `by_organization`, `by_approver_status`, `by_execution`, `by_org_status`, `by_resource` - ---- - -## Public API - -All public functions live under `api.workflow.api.*`. - -### Management (`api/management.ts`) - -- `createWorkflow` (mutation) → returns `Id<'wfDefinitions'>` -- `updateWorkflow` (mutation) -- `deleteWorkflow` (mutation) -- `listWorkflows` (query) -- `getWorkflow` (query) -- `activateWorkflow` (mutation) -- `deactivateWorkflow` (mutation) -- `duplicateWorkflow` (mutation) - -Step wrappers: - -- `createStep` (mutation) → returns `Id<'wfStepDefs'>` -- `updateStep` (mutation) -- `deleteStep` (mutation) -- `listWorkflowSteps` (query) -- `getOrderedSteps` (query) -- `reorderSteps` (mutation) - -### Execution (`api/execution.ts`) - -- Start: `startWorkflow` (mutation) → returns `Id<'wfExecutions'>` -- Manual trigger: `triggerWorkflow` (mutation; requires `triggers.manual`) -- Status: `getExecutionStatus` (action) by execution handle -- Cancel: `cancelExecution` (action) → marks as failed with `error='cancelled'` -- Read: `getExecution` (query), `listExecutions` (query) -- Stats: `getExecutionStats` (query) → totals, success rate, avg duration, last execution - -### Approvals (`nodes/approval/api.ts`) - -- `createApprovalTask` (mutation) -- `getApprovalTask` (query) -- `listPendingApprovals` (query) -- `updateApprovalStatus` (mutation) -- `approveWorkflowStep` (mutation) -- `rejectWorkflowStep` (mutation) -- `getApprovalHistory` (query) - ---- - -## Execution engines - -### Adapter to component engine (`adapter/component.ts`) - -- `startWithComponent` creates a `wfExecutions` row, then starts a component workflow via `workflowManager.start` using `dynamicWorkflow`, storing the returned `componentWorkflowId` on the execution. Also logs `component_workflow_created`. -- `componentDriver` is invoked by the component pool; it calls `runComponentDriver`, which: - - Loads the component journal - - Determines last completed step result - - Merges returned `variables` into `wfExecutions.variables`, keeping `organizationId` - - Routes using ports: `nextStepId = nextSteps[port]` (no stepType-specific branching, no default fallback) - - Marks execution as waiting (`waitingFor='approval'`) while keeping `status='running'` when approval steps are waiting for a decision - - Completes or fails the execution when no next step or on error - - Starts the next step by recording a journal entry that calls `internal.workflow.core.executor.executeStep` -- Completion hooks mirror the final component status to `wfExecutions`: - - `onWorkflowComplete` - -### Dynamic manager (`manager.ts`) - -- Defines `dynamicWorkflow` with `WorkflowManager` to iterate ordered steps and call `internal.workflow.core.executor.executeStep`. Marks execution completed/failed via internal mutations. - ---- - -## Node executors - -- Trigger (`nodes/trigger/executor.ts`): supports `manual | schedule | webhook | event`. Merges `triggerData` and context into variables. Uses `replaceVariables` for templating. -- LLM (`nodes/llm/executor.ts`): executor present; configure via `LLMNodeConfig` types. -- Condition (`nodes/condition/executor.ts`): evaluates expressions using JEXL (JavaScript Expression Language) for safe, powerful condition evaluation. -- Approval (`nodes/approval/executor.ts`): creates an approval task and returns `{ approvalTaskId }`. -- Action (`nodes/action/executor.ts`): resolves action by type via registry (`nodes/action/actions/*`) and executes it; basic retry policy placeholders. - -Step results follow `StepExecutionResult` and include `port`, `variables`, `output`, and may include `threadId`, `approvalTaskId`. - -### Loop Node - -The Loop node provides explicit, predictable iteration in workflows. Unlike implicit iteration styles (e.g., in some tools), Loop is an intentional step with clear routing and rich context. It supports single-item or batched processing and includes safety guards to prevent infinite loops. - -- Ports: `loop` (continue processing) and `done` (no more items) -- Suited for: bulk operations, rate-limited APIs, chunked processing, controlled retries - -#### Configuration - -```typescript -interface LoopNodeConfig { - batchSize: number; // Size of each batch; default 1 - maxIterations?: number; // Safety cap; default 1000 - itemVariable?: string; // Name for current item/batch; default 'item' - indexVariable?: string; // Name for current index; default 'index' - continueOnError?: boolean; // Continue when a downstream step fails; default false - description?: string; // Optional description -} -``` - -#### Flow (high level) - -```mermaid -graph TD - A[Input data] - B[Loop] - C{Has more items?} - D[Done port] - E[Make batch] - F[Loop port] - G["Downstream step(s)"] - H[Back to Loop] - I[Collect results] - - A --> B - B --> C - C -->|No| D - C -->|Yes| E - E --> F - F --> G - G --> H - H --> I - I --> C -``` - -#### Context and variables - -During execution, Loop exposes template variables: - -- `{{}}` – The current item (for batchSize=1) or current batch (an array when batchSize>1) -- `{{}}` – The zero-based index of the item/batch -- `{{loopContext.currentIndex}}` – Current item index -- `{{loopContext.batchIndex}}` – Current batch index -- `{{loopContext.totalItems}}` – Total number of items -- `{{loopContext.totalBatches}}` – Total number of batches -- `{{loopContext.processedCount}}` – Number of items processed so far -- `{{loopContext.remainingItems}}` – Number of items remaining -- `{{loopContext.isFirstBatch}}` – Whether this is the first batch -- `{{loopContext.isLastBatch}}` – Whether this is the last batch - -#### Examples - -Basic per-item iteration: - -```json -{ - "stepSlug": "loop_customers", - "stepType": "loop", - "config": { - "batchSize": 1, - "itemVariable": "customer", - "indexVariable": "customerIndex" - }, - "nextSteps": { - "loop": "send_email", - "done": "finish" - } -} -``` - -Batched iteration (useful for rate limiting): - -```json -{ - "stepSlug": "loop_batches", - "stepType": "loop", - "config": { - "batchSize": 5, - "maxIterations": 100, - "itemVariable": "batch", - "continueOnError": false - }, - "nextSteps": { - "loop": "api_call", - "done": "complete" - } -} -``` - -### Condition Node Configuration - -The condition node uses **JEXL (JavaScript Expression Language)** for safe, powerful expression evaluation. - -#### JEXL Expressions (Recommended) - -Use the `expression` property with a JEXL expression string: - -```json -{ - "stepType": "condition", - "config": { - "expression": "steps.assess_churn.output.isChurned == true" - } -} -``` - -**Benefits:** - -- **Safe**: No arbitrary code execution, sandboxed expression evaluation -- **Powerful**: Supports complex logic, comparisons, math, array operations, and transforms -- **Readable**: Natural JavaScript-like syntax -- **Flexible**: Direct variable access or template syntax `{{...}}` - -**Common JEXL Operations:** - -```javascript -// Comparison operations -'value1 == value2'; // Equality -'score > 0.8'; // Greater than -'age >= 18 && verified == true'; // Logical AND - -// Logical operations -'isChurned == true && score > 0.8'; -"status == 'active' || status == 'trial'"; -'!(disabled == true)'; - -// Array operations -"['active', 'pending', 'trial'].includes(status)"; -'items.length > 0'; -'items[0].price > 100'; - -// String operations -"name|upper == 'JOHN'"; // Transform to uppercase -'description|trim|length > 10'; // Chain transforms -"email.includes('@example.com')"; - -// Math operations -'price * quantity > 1000'; -'(subtotal + tax) * 0.9'; - -// Ternary operator -"age >= 18 ? 'adult' : 'minor'"; -``` - -#### Template Variable Syntax - -You can use `{{...}}` template syntax for variable substitution: - -```json -{ - "stepType": "condition", - "config": { - "expression": "{{steps.assess_churn.output.isChurned}} == true" - } -} -``` - -**Note:** When using `{{...}}` template syntax, the expression inside is evaluated as JEXL. Use `==` for equality comparisons: - -- ✅ Correct: `{{loop.item.status == "active"}}` -- ✅ Also correct: `loop.item.status == "active"` (without template syntax) -- ❌ Incorrect: `{{loop.item.status === "active"}}` (JEXL uses `==`, not `===`) - -#### Variable Access - -JEXL expressions can access any variable in the workflow context: - -- `steps.stepSlug.output.field` - Output from previous steps -- `workflow.variables.field` - Workflow-level variables -- `workflow.metadata.field` - Workflow metadata -- `loop.item` - Current loop item (in loop contexts) -- `loop.index` - Current loop index (in loop contexts) - -#### Real-World Examples - -**Customer Churn Detection:** - -```json -{ - "stepType": "condition", - "config": { - "expression": "steps.assess_churn.output.isChurned == true && steps.assess_churn.output.score > 0.85 && ['low_activity', 'payment_issues'].includes(steps.assess_churn.output.reason)" - } -} -``` - -**Subscription Validation:** - -```json -{ - "stepType": "condition", - "config": { - "expression": "steps.fetch_customer.output.customer.subscriptionCount == 0 || !steps.fetch_customer.output.customer.metadata.subscriptions" - } -} -``` - -**Loop Item Filtering:** - -```json -{ - "stepType": "condition", - "config": { - "expression": "loop.item.status == 'active' && loop.item.price > 100" - } -} -``` - -**Complex Business Logic:** - -```json -{ - "stepType": "condition", - "config": { - "expression": "(steps.check.output.status == 'active' && steps.check.output.verified == true) || steps.check.output.role == 'admin'" - } -} -``` - ---- - -## Variables and flow - -- Variables are initialized from `input` plus `organizationId`. -- After a successful step, returned `variables` are merged into `wfExecutions.variables`. The adapter also sets: - - `lastOutput` to the last step's output - - `steps[].output` structured map -- Routing semantics (ports-based): - - `trigger`: `success` - - `action`/`llm`: `success` or `failure` - - `condition`: `true` or `false` - - `approval`: `approve` or `reject` (marks execution as waiting: `waitingFor='approval'`) - - `loop`: `loop` or `done` - - Fallback: `default` - -### Secure Variables - -The `set_variables` action supports a `secure` flag for handling encrypted credentials: - -**Features:** - -- Variables with `secure: true` are automatically decrypted using the workflow's encryption key -- Decrypted values are stored in the `secrets` namespace (`secrets[name]`) -- Accessible via `{{secrets.name}}` template syntax -- Provides better security isolation for sensitive data - -**Example:** - -```typescript -{ - stepSlug: 'set_credentials', - stepType: 'action', - config: { - type: 'set_variables', - parameters: { - variables: [ - { - name: 'username', - value: '{{provider.username}}', - }, - { - name: 'password', - value: '{{provider.passwordEncrypted}}', - secure: true, // Automatically decrypt and store in secrets - }, - ], - }, - }, -} -``` - -**Usage in subsequent steps:** - -```typescript -{ - stepSlug: 'connect', - stepType: 'action', - config: { - type: 'imap', - parameters: { - username: '{{username}}', - password: '{{secrets.password}}', // Access from secrets namespace - }, - }, -} -``` - -**Security Benefits:** - -- ✅ Encrypted values remain encrypted until needed -- ✅ Decryption happens automatically in secure context -- ✅ Secrets stored separately from regular variables -- ✅ Clear intent that data is sensitive -- ✅ Easier to audit secret usage - ---- - -## Scheduler - -- `convex/crons.ts` registers: every 5 minutes call `internal.workflow.api.scheduler.scanAndTrigger` -- `api/scheduler.ts`: - - `scanAndTrigger` finds active workflows with `triggers.schedule` and starts them when due - - Maintenance utilities: `cleanupOldExecutions`, `generateWeeklyReport`, `monthlyMaintenance`, `archiveUnusedWorkflows` - ---- - -## Logs and stats - -- `getExecutionStepJournal` reads rich step-by-step execution journals from the `@convex-dev/workflow` component -- `getExecutionStats` returns counts and average execution time for recent executions - ---- - -## Client usage - -Prerequisites: - -```tsx -'use client'; -import { useQuery, useMutation, useAction } from 'convex/react'; -import { api } from '@/convex/_generated/api'; -import { Id } from '@/convex/_generated/dataModel'; -``` - -Template management example: - -```tsx -const workflows = useQuery(api.workflow.api.management.listWorkflows, { - organizationId, -}); -const createWorkflow = useMutation(api.workflow.api.management.createWorkflow); -const activateWorkflow = useMutation( - api.workflow.api.management.activateWorkflow, -); -const wfDefinitionId = await createWorkflow({ - organizationId, - name: 'My Workflow', - description: 'Example template', - triggers: { manual: true }, -}); -await activateWorkflow({ wfDefinitionId }); -``` - -Start workflow and poll status: - -```tsx -const startWorkflow = useMutation(api.workflow.api.execution.startWorkflow); -const getExecutionStatus = useAction( - api.workflow.api.execution.getExecutionStatus, -); -const handle = await startWorkflow({ - organizationId, - wfDefinitionId, - input: { any: 'data' }, - triggeredBy: 'manual', - triggerData: { - triggerType: 'manual', - timestamp: Date.now(), - }, -}); -const status = await getExecutionStatus({ handle }); -``` - -Cancel: - -```tsx -const cancelExecution = useAction(api.workflow.api.execution.cancelExecution); -await cancelExecution({ handle }); -``` - -Read execution and stats: - -```tsx -const execution = useQuery(api.workflow.api.execution.getExecution, { - executionId, -}); -const stats = useQuery(api.workflow.api.execution.getExecutionStats, { - wfDefinitionId, -}); -``` - -Step authoring (wrappers): - -```tsx -const createStep = useMutation(api.workflow.api.management.createStep); -await createStep({ - organizationId, - wfDefinitionId, - stepSlug: 'my_step', - name: 'My Step', - stepType: 'action', - order: 1, - config: { type: 'log', parameters: { message: 'Hello' } }, - nextSteps: { success: 'next_step_id' }, -}); -``` - -## 🆕 Inline Workflow Execution - -Execute workflows with manually passed configurations, bypassing database storage: - -```tsx -const executeWorkflowWithConfig = useMutation( - api.workflow.api.execution.executeWorkflowWithConfig, -); - -const executionId = await executeWorkflowWithConfig({ - organizationId, - workflowConfig: { - name: 'Dynamic Workflow', - description: 'Generated at runtime', - version: '1.0.0', - }, - stepsConfig: [ - { - stepSlug: 'trigger_start', - name: 'Start', - stepType: 'trigger', - order: 1, - config: { type: 'manual' }, - nextSteps: { default: 'llm_step' }, - }, - { - stepSlug: 'llm_step', - name: 'Process with LLM', - stepType: 'llm', - order: 2, - config: { - name: 'Processor', - model: 'gpt-4o-mini', - systemPrompt: 'You are a helpful assistant.', - userPrompt: 'Process: {{input}}', - }, - nextSteps: {}, - }, - ], - input: { data: 'Hello world' }, - triggeredBy: 'api_call', -}); -``` - -**Key Benefits:** - -- No database dependency for workflow definitions -- Dynamic workflow generation at runtime -- Perfect for testing and prototyping -- Identical execution behavior to database workflows -- Full logging and monitoring support - -**Use Cases:** - -- Testing workflow logic -- Dynamic workflows based on runtime conditions -- External system integration -- A/B testing different configurations -- One-time or temporary executions - -**Advanced Example with Conditional Logic:** - -```tsx -const executionId = await executeWorkflowWithConfig({ - organizationId, - workflowConfig: { - name: 'Content Moderation', - description: 'Analyze and moderate content', - }, - stepsConfig: [ - { - stepSlug: 'trigger_start', - name: 'Content Received', - stepType: 'trigger', - order: 1, - config: { type: 'manual' }, - nextSteps: { default: 'analyze_content' }, - }, - { - stepSlug: 'analyze_content', - name: 'Analyze Safety', - stepType: 'llm', - order: 2, - config: { - model: 'gpt-4o', - systemPrompt: 'Analyze content safety. Return "safe" or "unsafe".', - userPrompt: 'Analyze: {{content}}', - }, - nextSteps: { default: 'safety_check' }, - }, - { - stepSlug: 'safety_check', - name: 'Safety Decision', - stepType: 'condition', - order: 3, - config: { - expression: 'analysisResult == "safe"', - }, - nextSteps: { - true: 'approve_content', - false: 'flag_content', - }, - }, - ], - input: { content: 'Content to moderate...' }, - triggeredBy: 'content_upload', -}); -``` - ---- - -## Behavior and constraints - -- `startWorkflow`/`triggerWorkflow` return the execution id (handle) -- `startWorkflow` throws when workflow `status !== 'active'` -- `triggerWorkflow` throws when `triggers.manual` is not enabled -- Approval steps can pause execution by setting `waitingFor='approval'` while keeping `status='running'` -- Variables merge after successful steps; `organizationId` is preserved -- Adapter journaling exposes `lastOutput` and per-step outputs in `variables.steps` - -Known limitations (current state): - -- `approveWorkflowStep`/`rejectWorkflowStep` update tasks and schedule processing. In rare edge cases, internal tooling can call `internal.wf_executions.resumeExecution` to move executions back to `running`. There is no public resume action. - ---- - -## Appendix - -- Public entry points: `api/management.ts`, `api/execution.ts` -- Internal-only areas: `database/`, `nodes/`, `utils/` -- Adapter bridge: `adapter/component.ts` (start, driver, completion) diff --git a/docs/workflows/entity-finder-prompt-design.md b/docs/workflows/entity-finder-prompt-design.md deleted file mode 100644 index 128ae34989..0000000000 --- a/docs/workflows/entity-finder-prompt-design.md +++ /dev/null @@ -1,279 +0,0 @@ -# Entity Finder Agent - Prompt Design - -## 概述 - -Entity Finder Agent 使用了清晰的 **System Prompt** 和 **User Prompt** 分离设计,遵循以下原则: - -- **System Prompt**:技术性指令,告诉 AI 如何工作(工具使用、参数、数据结构) -- **User Prompt**:业务意图,告诉 AI 用户想要什么(模糊的、高层次的) - -## 设计原则 - -### System Prompt 的职责 - -System Prompt 应该包含所有**技术细节**,让 AI 知道: - -1. **角色定义**:你是什么 agent,负责什么 -2. **可用工具**:有哪些工具可以使用 -3. **工具参数**:每个参数的含义和**必须使用的值** -4. **工具返回**:返回数据的结构和含义 -5. **工作流程**:一步步应该做什么 -6. **特殊协议**:如终止信号的格式 -7. **示例**:完整的输入输出示例 - -**关键点**:System Prompt 应该强化到让 AI 无需猜测任何技术细节。 - -### User Prompt 的职责 - -User Prompt 应该是**业务导向**的,表达用户的意图: - -- ✅ "Find a customer who needs status assessment" -- ✅ "Find an entity that needs processing" -- ✅ "Get me a customer to analyze" - -**不应该包含**: - -- ❌ 技术参数(entityType, workflowId, daysBack) -- ❌ 工具名称(find_unprocessed_entities) -- ❌ 数据结构(count, entities array) -- ❌ 实现细节("use limit: 1") - -## 实现 - -### 配置结构 - -```typescript -export interface EntityFinderConfig { - agentType: 'entity_finder'; - - // 技术参数(用于构建 system prompt) - entityType: 'customer' | 'product' | 'subscription'; - workflowId?: string; - daysBack?: number; - - // 用户意图(业务导向) - userPrompt?: string; - - // 可选的自定义 system prompt - systemPrompt?: string; -} -``` - -### System Prompt 构建 - -System Prompt 是动态生成的,将配置参数注入到模板中: - -```typescript -function buildSystemPrompt(config: { - entityType: string; - workflowId: string; - daysBack: number; -}): string { - return `You are an Entity Finder Agent... - - Tool Parameters (YOU MUST USE THESE EXACT VALUES): - • entityType: "${config.entityType}" - • workflowId: "${config.workflowId}" - • daysBack: ${config.daysBack} - • limit: 1 (ALWAYS use 1) - - ...`; -} -``` - -**优势**: - -- AI 不需要从 user prompt 中提取参数 -- 参数值是明确的、不可变的 -- 减少了 AI 出错的可能性 - -### User Prompt 使用 - -```typescript -const { - entityType, - workflowId = context.workflowKey, - daysBack = 3, - userPrompt = DEFAULT_USER_PROMPT, // 默认值:简单模糊 - systemPrompt, -} = config; - -// 如果用户没有提供 system prompt,使用动态生成的 -const finalSystemPrompt = - systemPrompt || buildSystemPrompt({ entityType, workflowId, daysBack }); -``` - -## Workflow 配置示例 - -### 示例 1:使用默认 User Prompt - -```typescript -{ - stepSlug: 'customer_finder', - stepType: 'agent', - config: { - agent: { - agentType: 'entity_finder', - entityType: 'customer', - // userPrompt 使用默认值:"Find an entity that needs processing." - }, - }, - nextSteps: { - success: 'status_analyzer', - terminate: 'finish', - }, -} -``` - -### 示例 2:自定义 User Prompt - -```typescript -{ - stepSlug: 'customer_finder', - stepType: 'agent', - config: { - agent: { - agentType: 'entity_finder', - entityType: 'customer', - workflowId: 'assess-customer-status', - daysBack: 7, - userPrompt: 'Find a customer who needs their subscription status assessed', - }, - }, - nextSteps: { - success: 'status_analyzer', - terminate: 'finish', - }, -} -``` - -### 示例 3:完全自定义(高级用法) - -```typescript -{ - stepSlug: 'customer_finder', - stepType: 'agent', - config: { - agent: { - agentType: 'entity_finder', - entityType: 'customer', - userPrompt: 'Find a high-value customer for churn analysis', - systemPrompt: `Custom system prompt with special instructions...`, - }, - }, - nextSteps: { - success: 'churn_analyzer', - terminate: 'finish', - }, -} -``` - -## 对比:改进前 vs 改进后 - -### 改进前(❌ 混淆) - -```typescript -// User prompt 包含了太多技术细节 -const userPrompt = `Find ONE ${entityType} that needs processing. - -Search parameters: -- Entity type: ${entityType} -- Workflow ID: ${workflowId} -- Days back: ${daysBack} -- Limit: 1 - -Use the find_unprocessed_entities tool to search...`; -``` - -**问题**: - -- User prompt 变成了技术指令 -- 用户无法自定义查询意图 -- 混淆了"用户想要什么"和"如何实现" - -### 改进后(✅ 清晰) - -```typescript -// System Prompt:技术细节,强化指令 -const systemPrompt = buildSystemPrompt({ entityType, workflowId, daysBack }); -// 包含:工具参数、数据结构、工作流程、示例 - -// User Prompt:业务意图,简单模糊 -const userPrompt = config.userPrompt || 'Find an entity that needs processing.'; -``` - -**优势**: - -- 职责分离清晰 -- 用户可以自定义业务意图 -- AI 有明确的技术指导 -- 减少出错可能性 - -## 最佳实践 - -### 1. 默认情况下不需要自定义 - -大多数情况下,只需要配置技术参数: - -```typescript -{ - agentType: 'entity_finder', - entityType: 'customer', - // 其他都用默认值 -} -``` - -### 2. 自定义 User Prompt 表达业务意图 - -如果需要更具体的查询意图: - -```typescript -{ - agentType: 'entity_finder', - entityType: 'customer', - userPrompt: 'Find a customer who recently became inactive', -} -``` - -### 3. 只在特殊情况下自定义 System Prompt - -只有在需要完全不同的行为时才自定义 system prompt: - -```typescript -{ - agentType: 'entity_finder', - entityType: 'customer', - systemPrompt: `You are a specialized customer finder... - [Completely different instructions]`, -} -``` - -## 总结 - -这个设计遵循了 AI prompt engineering 的最佳实践: - -1. **System Prompt = 技术指令** - - - 详细、明确、强化 - - 包含所有工具使用细节 - - 注入配置参数 - - 提供完整示例 - -2. **User Prompt = 业务意图** - - - 简单、模糊、高层次 - - 表达用户想要什么 - - 不关心实现细节 - - 可以在 workflow 中自定义 - -3. **Config = 技术参数** - - 用于构建 system prompt - - 明确的、类型安全的 - - 有合理的默认值 - -这样的设计让 AI 能够: - -- 理解用户的业务意图(从 user prompt) -- 知道如何正确使用工具(从 system prompt) -- 使用正确的参数(从 config 注入到 system prompt) -- 返回正确的结果(从 system prompt 的示例) diff --git a/docs/workflows/find-unprocessed-entities-tool.md b/docs/workflows/find-unprocessed-entities-tool.md deleted file mode 100644 index ecbb681c0e..0000000000 --- a/docs/workflows/find-unprocessed-entities-tool.md +++ /dev/null @@ -1,331 +0,0 @@ -# find_unprocessed_entities Tool - -## Overview - -The `find_unprocessed_entities` tool is a specialized Convex tool that finds entities (currently only customers) that haven't been processed by a specific workflow within a given time window. - -## Purpose - -This tool enables workflows to automatically discover entities that need processing, based on workflow-specific tracking metadata. It's the foundation of the auto-processing pattern for agent orchestration workflows. - -## How It Works - -### Tracking Mechanism - -Each workflow tracks its processing history in the entity's metadata: - -```typescript -// Customer metadata structure -{ - metadata: { - workflows: { - "assess-customer-status": { - lastProcessedAt: "2024-01-15T10:30:00Z", - lastExecutionId: "exec_123" - }, - "send-churn-survey": { - lastProcessedAt: "2024-01-10T08:00:00Z", - lastExecutionId: "exec_456" - } - } - } -} -``` - -### Search Logic - -The tool finds entities where: - -1. `metadata.workflows.{workflowId}.lastProcessedAt` is **missing** (never processed), OR -2. `metadata.workflows.{workflowId}.lastProcessedAt` is **older** than `daysBack` days ago - -## Tool Signature - -```typescript -find_unprocessed_entities({ - entityType: 'customer', // Currently only 'customer' is supported - workflowId: string, // e.g., 'assess-customer-status' - daysBack: number, // Default: 3 days - limit: number, // Default: 1, max: 100 -}); -``` - -### Parameters - -| Parameter | Type | Required | Default | Description | -| ------------ | ------------ | -------- | ------------ | ------------------------------------------------------------ | -| `entityType` | `'customer'` | No | `'customer'` | Type of entity to search (only customer supported currently) | -| `workflowId` | `string` | Yes | - | Workflow ID to check processing history for | -| `daysBack` | `number` | No | `3` | Number of days to look back (1-365) | -| `limit` | `number` | No | `1` | Maximum number of entities to return (1-100) | - -### Return Value - -```typescript -{ - entities: Array>, // Array of customer documents - count: number, // Number of entities found - searchCriteria: { - entityType: string, - workflowId: string, - daysBack: number, - cutoffDate: string // ISO timestamp - } -} -``` - -## Usage in Workflows - -### Example: Customer Finder Agent - -```typescript -{ - stepSlug: 'customer_finder_agent', - stepType: 'llm', - config: { - llmNode: { - systemPrompt: `You are a Customer Finder Agent. - -Use find_unprocessed_entities tool to find ONE customer that needs processing. - -If count > 0: Return the first customer from entities array -If count = 0: Return termination signal`, - - userPrompt: `Find ONE customer for workflow: {{workflowId}} -Days back: {{daysBack}}`, - - tools: ['find_unprocessed_entities'], - }, - }, -} -``` - -### Example AI Usage - -**AI Call:** - -```json -{ - "tool": "find_unprocessed_entities", - "args": { - "entityType": "customer", - "workflowId": "assess-customer-status", - "daysBack": 3, - "limit": 1 - } -} -``` - -**Tool Response (customer found):** - -```json -{ - "entities": [ - { - "_id": "cust_123", - "name": "John Doe", - "email": "john@example.com", - "metadata": { - "workflows": { - "other-workflow": { - "lastProcessedAt": "2024-01-10T00:00:00Z" - } - } - } - } - ], - "count": 1, - "searchCriteria": { - "entityType": "customer", - "workflowId": "assess-customer-status", - "daysBack": 3, - "cutoffDate": "2024-01-12T00:00:00Z" - } -} -``` - -**Tool Response (no customers found):** - -```json -{ - "entities": [], - "count": 0, - "searchCriteria": { - "entityType": "customer", - "workflowId": "assess-customer-status", - "daysBack": 3, - "cutoffDate": "2024-01-12T00:00:00Z" - } -} -``` - -## Implementation Details - -### Files Created - -1. **Tool Definition** - - - `convex/workflow/nodes/llm/tools/convex_tools/find_unprocessed_entities.ts` - - Defines the tool interface and validation - -2. **Query Implementation** - - - `convex/customer_queries/find_unprocessed.ts` - - Implements the actual database query logic - -3. **Tool Registration** - - Updated `convex/workflow/nodes/llm/tools/tool_registry.ts` - - Registered the tool in the global registry - -### Query Logic - -```typescript -// Simplified query logic -export const findUnprocessed = internalQuery({ - handler: async (ctx, args) => { - const { organizationId, workflowId, cutoffTimestamp, limit } = args; - - // Get all customers for organization - const allCustomers = await ctx.db - .query('customers') - .withIndex('by_organization', (q) => - q.eq('organizationId', organizationId), - ) - .collect(); - - // Filter based on workflow processing status - const unprocessed = []; - for (const customer of allCustomers) { - const lastProcessedAt = - customer.metadata?.workflows?.[workflowId]?.lastProcessedAt; - - if (!lastProcessedAt || lastProcessedAt < cutoffTimestamp) { - unprocessed.push(customer); - if (unprocessed.length >= limit) break; - } - } - - return { entities: unprocessed, count: unprocessed.length }; - }, -}); -``` - -## Benefits - -1. **Workflow-Specific Tracking** - - - Each workflow tracks its own processing independently - - No conflicts between different workflows - -2. **Flexible Time Windows** - - - Configurable `daysBack` parameter - - Prevents over-processing or under-processing - -3. **Efficient Discovery** - - - Finds entities that need work automatically - - No manual intervention required - -4. **Audit Trail** - - Complete history of when each workflow processed each entity - - Includes execution IDs for debugging - -## Best Practices - -1. **Use Descriptive Workflow IDs** - - ```typescript - workflowId: 'assess-customer-status'; // ✅ Good - workflowId: 'workflow1'; // ❌ Bad - ``` - -2. **Set Appropriate Time Windows** - - ```typescript - daysBack: 1; // For frequent checks (daily) - daysBack: 7; // For weekly reviews - daysBack: 30; // For monthly analysis - ``` - -3. **Limit Results Appropriately** - - ```typescript - limit: 1; // ✅ For one-at-a-time processing - limit: 10; // ✅ For batch processing - limit: 100; // ⚠️ Maximum allowed - ``` - -4. **Always Update Processing Metadata** - After processing an entity, always update: - ```typescript - metadata.workflows.{workflowId}.lastProcessedAt = new Date().toISOString() - metadata.workflows.{workflowId}.lastExecutionId = executionId - ``` - -## Future Enhancements - -### Planned Features - -1. **Support for More Entity Types** - - - Products - - Subscriptions - - Orders - - Custom entities - -2. **Advanced Filtering** - - - Additional filter criteria beyond time - - Status-based filtering - - Tag-based filtering - -3. **Performance Optimization** - - - Index-based queries for large datasets - - Pagination support - - Caching strategies - -4. **Batch Processing** - - Process multiple entities in one execution - - Configurable batch sizes - - Progress tracking - -## Troubleshooting - -### No Entities Found - -**Problem:** Tool always returns `count: 0` - -**Solutions:** - -1. Check if entities exist in the database -2. Verify `workflowId` matches exactly -3. Check if `daysBack` is too short -4. Verify `organizationId` is correct - -### Too Many Entities Found - -**Problem:** Tool returns many entities when you expect few - -**Solutions:** - -1. Reduce `daysBack` value -2. Ensure processing metadata is being updated correctly -3. Check for duplicate workflow executions - -### Type Errors - -**Problem:** TypeScript errors about missing properties - -**Solutions:** - -1. Run `npx convex dev` to regenerate types -2. Ensure all files are saved -3. Restart TypeScript server in IDE - -## Related Documentation - -- [Auto-Processing Pattern](./auto-processing-pattern.md) -- [Workflow Termination Protocol](../convex/workflow/nodes/llm/types/workflow_termination.ts) -- [Workflow Types](./workflow-types.md) diff --git a/docs/workflows/manual-configuration.md b/docs/workflows/manual-configuration.md deleted file mode 100644 index 789b469e15..0000000000 --- a/docs/workflows/manual-configuration.md +++ /dev/null @@ -1,224 +0,0 @@ -# Manual Workflow Configuration Execution - -This feature allows you to execute workflows by providing JSON configuration directly, bypassing the need for database-stored workflow definitions. This is useful for testing, prototyping, and one-off workflow executions. - -## How to Access - -1. Navigate to the Workflow Demo page: `/dashboard/{organizationId}/workflows-demo` -2. Click on the "Manual Configuration" tab -3. You'll see three main sections: - - **Workflow Configuration**: Define workflow metadata and global settings - - **Steps Configuration**: Define the workflow steps and their execution order - - **Execution Control**: Provide input data and execute the workflow - -## Configuration Structure - -### Workflow Configuration - -The workflow configuration defines metadata and global settings: - -```json -{ - "name": "My Test Workflow", - "description": "A workflow executed with manual configuration", - "version": "1.0.0", - "config": { - "timeout": 300000, - "retryPolicy": { - "maxRetries": 3, - "backoffMs": 1000 - }, - "variables": { - "environment": "test" - } - } -} -``` - -### Steps Configuration - -The steps configuration is an array of step definitions: - -```json -[ - { - "stepSlug": "trigger_1", - "name": "Manual Trigger", - "stepType": "trigger", - "order": 1, - "config": { - "type": "manual", - "context": {} - }, - "nextSteps": { - "default": "action_1" - } - }, - { - "stepSlug": "action_1", - "name": "Test Action", - "stepType": "action", - "order": 2, - "config": { - "type": "log", - "message": "Hello from manual workflow execution!", - "level": "info" - }, - "nextSteps": {} - } -] -``` - -## Step Types - -The following step types are supported: - -### 1. Trigger Steps - -- **stepType**: `"trigger"` -- **Purpose**: Entry point for workflow execution -- **Config Example**: - ```json - { - "type": "manual", - "context": {} - } - ``` - -### 2. Action Steps - -- **stepType**: `"action"` -- **Purpose**: Perform actions like logging, API calls, etc. -- **Config Example**: - ```json - { - "type": "log", - "message": "Action executed", - "level": "info" - } - ``` - -### 3. Condition Steps - -- **stepType**: `"condition"` -- **Purpose**: Conditional branching based on expressions -- **Config Example**: - ```json - { - "expression": "input.value > 50" - } - ``` -- **Next Steps**: Use `onTrue` and `onFalse` for branching - -### 4. Approval Steps - -- **stepType**: `"approval"` -- **Purpose**: Require manual approval to continue -- **Config Example**: - ```json - { - "approverRole": "manager", - "message": "Please approve this workflow execution" - } - ``` -- **Next Steps**: Use `onApprove` and `onReject` for branching - -### 5. LLM Steps - -- **stepType**: `"llm"` -- **Purpose**: AI/LLM processing steps -- **Config Example**: - ```json - { - "name": "Analyze Data", - "systemPrompt": "You are a data analyst...", - "userPrompt": "Process this data: {{input}}", - "temperature": 0.7, - "tools": [], - "outputFormat": "json" - } - ``` -- **Required Fields**: `name`, `systemPrompt` -- **Optional Fields**: `userPrompt` (recommended), `temperature`, `tools`, `outputFormat`, `maxSteps`, `maxTokens` -- **Model Selection**: The model is configured globally via the `OPENAI_MODEL` environment variable (required; no default model is provided) and cannot be customized per step. - -## Next Steps Configuration - -Each step must define how to proceed to the next step: - -- **default**: Default next step (used for most step types) -- **onSuccess**: Next step on successful execution (used for some step types) -- **onTrue**: Next step when condition is true -- **onFalse**: Next step when condition is false -- **onApprove**: Next step when approval is granted -- **onReject**: Next step when approval is rejected - -**Note**: Action nodes do not have failure ports. If an action fails, it throws an exception and the workflow execution fails. Use workflow-level error handling (retries, timeouts) instead of per-action failure paths. - -## Example Templates - -The interface provides three example templates: - -### 1. Simple Example - -A basic workflow with a trigger and a log action. - -### 2. Complex Example - -A workflow with conditional branching based on input values. - -### 3. Approval Example - -A workflow that requires manual approval before proceeding. - -## Execution Input - -Provide JSON input data that will be available to all steps in the workflow: - -```json -{ - "testData": "Manual execution test", - "timestamp": "2024-01-15T10:30:00Z", - "value": 75 -} -``` - -## Backend API - -The feature uses the `executeWorkflowWithConfig` mutation: - -```typescript -const executionId = await executeWorkflowWithConfig({ - organizationId, - workflowConfig: parsedWorkflowConfig, - stepsConfig: parsedStepsConfig, - input: parsedInput, - triggeredBy: 'manual-config', - triggerData: { - triggerType: 'manual-config', - timestamp: Date.now(), - }, -}); -``` - -## Benefits - -1. **Rapid Prototyping**: Test workflow logic without creating database entries -2. **One-off Executions**: Execute workflows for specific scenarios -3. **Development & Testing**: Validate workflow configurations before saving -4. **Debugging**: Test specific step configurations in isolation -5. **Integration Testing**: Verify workflow behavior with different inputs - -## Error Handling - -The interface validates JSON syntax and provides helpful error messages for: - -- Invalid JSON in workflow configuration -- Invalid JSON in steps configuration -- Invalid JSON in execution input -- Missing required fields -- Invalid step type configurations - -## Monitoring - -After execution, you can monitor the workflow using the standard workflow monitoring tools. The execution will appear in the execution history with the trigger type "manual-config". diff --git a/docs/workflows/version-management.md b/docs/workflows/version-management.md deleted file mode 100644 index 756e32c2d9..0000000000 --- a/docs/workflows/version-management.md +++ /dev/null @@ -1,44 +0,0 @@ -# Workflow Version Management (Minimal) - -## Overview - -- Lifecycle: Draft (mutable) → Active (immutable) → Archived (history) -- Publish promotes Draft → Active and automatically creates a new Draft (next version) -- Rollback re-activates a historical version and creates a new Draft from it - -## Public API (one file, one function) - -- create_workflow.ts → createWorkflow (mutation) -- update_workflow_draft.ts → updateWorkflowDraft (mutation) -- publish_workflow.ts → publishWorkflow (mutation) -- rollback_to_version.ts → rollbackToVersion (mutation) -- get_workflow_draft.ts → getWorkflowDraft (query) -- get_active_workflow.ts → getActiveWorkflow (query) -- get_workflow_version.ts → getWorkflowVersion (query) -- list_workflow_versions.ts → listWorkflowVersions (query) - -## Naming principles - -- One function per file -- File in snake_case; function in camelCase -- File name corresponds to function name - -## Quick usage - -```ts -// 1) Create draft -const draftId = await convex.mutation( - api.wf_definitions.createWorkflowDraft, - args, -); -// 2) Publish draft → Active + new Draft -const r = await convex.mutation(api.wf_definitions.publishDraft, { - wfDefinitionId: draftId, - publishedBy: 'you', -}); -// 3) Read active -const active = await convex.query(api.wf_definitions.getActiveVersion, { - organizationId, - workflowKey, -}); -``` diff --git a/docs/workflows/workflow-patterns-guide.md b/docs/workflows/workflow-patterns-guide.md deleted file mode 100644 index 07920478a8..0000000000 --- a/docs/workflows/workflow-patterns-guide.md +++ /dev/null @@ -1,790 +0,0 @@ -# Workflow Patterns Guide - -This guide provides comprehensive patterns for creating workflows in the system. - -## Table of Contents - -1. [Workflow Types](#workflow-types) -2. [Entity Processing Pattern](#entity-processing-pattern) -3. [Data Sync Pattern](#data-sync-pattern) -4. [Templating and Expressions](#templating-and-expressions) -5. [Common Action Patterns](#common-action-patterns) -6. [Best Practices](#best-practices) - ---- - -## Workflow Types - -### 1. Entity Processing Workflows - -**Purpose**: Process entities one at a time (customers, products, conversations) - -**Characteristics**: - -- Process ONE entity per execution -- Use scheduled trigger for automated processing -- Track processed entities to avoid reprocessing -- Suitable for: customer analysis, product recommendations, conversation replies, status assessments - -**When to Use**: - -- When each entity requires significant processing (AI analysis, external API calls) -- When you want to avoid overwhelming external services -- When you need fine-grained control over processing rate -- When you want to track which entities have been processed - -### 2. Data Sync Workflows - -**Purpose**: Sync data from external sources (APIs, databases, files) - -**Characteristics**: - -- Process multiple items per execution -- Use pagination patterns -- Track sync state (cursors, timestamps, page info) -- Suitable for: Shopify sync, IMAP sync, website crawling, API data imports - -**When to Use**: - -- When syncing data from external APIs -- When processing large datasets in batches -- When you need to maintain sync state across executions - ---- - -## Entity Processing Pattern - -### Standard Structure - -```typescript -{ - workflowConfig: { - name: 'Entity Processing Workflow', - workflowType: 'predefined', - config: { - timeout: 120000, - variables: { - organizationId: 'org_demo', - workflowId: 'unique-workflow-id', - backoffHours: 168, // 7 days - }, - }, - }, - stepsConfig: [ - // Step 1: Scheduled Trigger - { - stepSlug: 'start', - name: 'Start', - stepType: 'trigger', - order: 1, - config: { - type: 'schedule', - schedule: '0 */2 * * *', // Every 2 hours - timezone: 'UTC', - }, - nextSteps: { success: 'find_unprocessed' }, - }, - - // Step 2: Find Unprocessed Entity - { - stepSlug: 'find_unprocessed', - name: 'Find Unprocessed Entity', - stepType: 'action', - order: 2, - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'find_unprocessed', - organizationId: '{{organizationId}}', - tableName: 'customers', // or 'products', 'conversations' - workflowId: '{{workflowId}}', - backoffHours: '{{backoffHours}}', - }, - }, - nextSteps: { success: 'check_found' }, - }, - - // Step 3: Check if Entity Found - { - stepSlug: 'check_found', - name: 'Check if Entity Found', - stepType: 'condition', - order: 3, - config: { - expression: 'steps.find_unprocessed.output.data.count > 0', - }, - nextSteps: { - true: 'extract_data', - false: 'noop', // No entities to process - }, - }, - - // Step 4: Extract Entity Data - { - stepSlug: 'extract_data', - name: 'Extract Entity Data', - stepType: 'action', - order: 4, - config: { - type: 'set_variables', - parameters: { - variables: [ - { name: 'entityId', value: '{{steps.find_unprocessed.output.data.documents[0]._id}}' }, - { name: 'entityData', value: '{{steps.find_unprocessed.output.data.documents[0]}}' }, - ], - }, - }, - nextSteps: { success: 'process_entity' }, - }, - - // Steps 5-N: Your Business Logic - // ... (analyze, generate, update, etc.) - - // Step N+1: Record as Processed - { - stepSlug: 'record_processed', - name: 'Record as Processed', - stepType: 'action', - order: 99, // Last step - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'record_processed', - organizationId: '{{organizationId}}', - tableName: 'customers', - workflowId: '{{workflowId}}', - documentId: '{{entityId}}', - documentCreationTime: '{{entityData._creationTime}}', - metadata: { - processedAt: '{{now}}', - // Additional metadata - }, - }, - }, - nextSteps: { success: 'noop' }, - }, - ], -} -``` - -### Key Points - -1. **Scheduled Trigger**: Use cron expression (e.g., `0 */2 * * *` for every 2 hours) -2. **Find Unprocessed**: Always check for unprocessed entities first -3. **Backoff Period**: Use `backoffHours` to avoid reprocessing recently processed entities -4. **Graceful Termination**: Use `noop` when no entities found -5. **Record Processed**: Always mark entity as processed at the end - ---- - -## Data Sync Pattern - -### Pagination Pattern - -```typescript -{ - workflowConfig: { - name: 'Data Sync Workflow', - config: { - variables: { - organizationId: 'org_demo', - pageSize: 50, - maxPages: 20, // Safety limit - currentPage: 0, - nextPageInfo: null, - }, - }, - }, - stepsConfig: [ - // Step 1: Trigger - { - stepSlug: 'start', - stepType: 'trigger', - order: 1, - config: { type: 'manual' }, - nextSteps: { success: 'fetch_page' }, - }, - - // Step 2: Fetch Page - { - stepSlug: 'fetch_page', - stepType: 'action', - order: 2, - config: { - type: 'shopify', // or other API - parameters: { - operation: 'list', - resource: 'products', - limit: '{{pageSize}}', - page_info: '{{nextPageInfo}}', - }, - }, - nextSteps: { success: 'loop_items' }, - }, - - // Step 3: Loop Through Items - { - stepSlug: 'loop_items', - stepType: 'loop', - order: 3, - config: { - items: '{{steps.fetch_page.output.data.data}}', - itemVariable: 'item', - }, - nextSteps: { - loop: 'process_item', - done: 'check_next_page', - }, - }, - - // Step 4: Process Each Item - { - stepSlug: 'process_item', - stepType: 'action', - order: 4, - config: { - type: 'product', - parameters: { - operation: 'create', - name: '{{loop.item.title}}', - externalId: '{{loop.item.id}}', - }, - }, - nextSteps: { success: 'loop_items' }, - }, - - // Step 5: Check if More Pages - { - stepSlug: 'check_next_page', - stepType: 'condition', - order: 5, - config: { - expression: 'steps.fetch_page.output.data.pagination.hasNextPage == true && currentPage < maxPages', - }, - nextSteps: { - true: 'update_pagination', - false: 'noop', - }, - }, - - // Step 6: Update Pagination Variables - { - stepSlug: 'update_pagination', - stepType: 'action', - order: 6, - config: { - type: 'set_variables', - parameters: { - variables: [ - { name: 'currentPage', value: '{{currentPage + 1}}' }, - { name: 'nextPageInfo', value: '{{steps.fetch_page.output.data.pagination.nextPageInfo}}' }, - ], - }, - }, - nextSteps: { success: 'fetch_page' }, // Loop back - }, - ], -} -``` - ---- - -## Templating and Expressions - -### Variable Interpolation - -```javascript -// Simple variables -'{{organizationId}}'; -'{{workflowId}}'; - -// Step outputs -'{{steps.fetch_data.output.data.customer.name}}'; -'{{steps.find_unprocessed.output.data.documents[0]._id}}'; - -// Secrets (automatically decrypted) -'{{secrets.apiKey}}'; -'{{secrets.imapPassword}}'; - -// Loop variables -'{{loop.item}}'; -'{{loop.item.id}}'; -'{{loop.state.iterations}}'; - -// Built-in variables -'{{now}}'; // Current ISO timestamp -'{{nowMs}}'; // Current timestamp in milliseconds -``` - -### JEXL Filters - -```javascript -// Array operations -'{{items|length}}'; // Get array length -'{{items|map("id")}}'; // Extract 'id' from each item -'{{items|unique}}'; // Remove duplicates -'{{items|flatten}}'; // Flatten nested arrays -'{{items|concat(otherArray)}}'; // Concatenate arrays -'{{items|find("id", "123")}}'; // Find item by property - -// String operations -'{{items|join(", ")}}'; // Join array with separator - -// Formatting -'{{items|formatList("Name: {name}", "\\n")}}'; // Format array as list - -// Boolean operations -'{{array1|hasOverlap(array2)}}'; // Check if arrays have common elements - -// Complex expressions -'{{customers|map("metadata.subscriptions.data")|flatten|map("product_id")|unique}}'; -``` - -### Condition Expressions - -```javascript -// Comparison -'variable == "active"'; -'steps.fetch.output.data.count > 0'; -'count >= 5'; - -// Boolean logic -'status == "active" && count > 5'; -'type == "A" || type == "B"'; -'!(status == "inactive")'; - -// Array operations -'items|length > 0'; -'steps.fetch.output.data.hasNextPage == true'; - -// Nested property access -'steps.query.output.data.customer.status == "active"'; - -// Complex conditions -'(metadata && metadata.subscriptions) ? metadata.subscriptions.data|length > 0 : false'; -``` - ---- - -## Common Action Patterns - -### workflow_processing_records - -**Find Unprocessed Entity:** - -```typescript -{ - stepType: 'action', - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'find_unprocessed', - organizationId: '{{organizationId}}', - tableName: 'customers', // or 'products', 'conversations' - workflowId: '{{workflowId}}', - backoffHours: 168, // Don't reprocess for 7 days - }, - }, -} -``` - -**Find Unprocessed Open Conversation (special operation):** - -```typescript -{ - stepType: 'action', - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'find_unprocessed_open_conversation', - organizationId: '{{organizationId}}', - workflowId: '{{workflowId}}', - backoffHours: '{{backoffHours}}', - }, - }, -} -``` - -**Record as Processed:** - -```typescript -{ - stepType: 'action', - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'record_processed', - organizationId: '{{organizationId}}', - tableName: 'customers', - workflowId: '{{workflowId}}', - documentId: '{{entityId}}', - documentCreationTime: '{{entity._creationTime}}', - metadata: { - processedAt: '{{now}}', - result: 'success', - }, - }, - }, -} -``` - -### set_variables - -**Extract and Store Data:** - -```typescript -{ - stepType: 'action', - config: { - type: 'set_variables', - parameters: { - variables: [ - { name: 'customerId', value: '{{steps.find.output.data.documents[0]._id}}' }, - { name: 'customerName', value: '{{steps.find.output.data.documents[0].name}}' }, - { name: 'apiKey', value: '{{encryptedKey}}', secure: true }, // Encrypted - ], - }, - }, -} -``` - -**Note**: Use `secure: true` to automatically decrypt encrypted values and store them in secrets namespace. - -### LLM Steps - -**AI Analysis with Tools:** - -```typescript -{ - stepType: 'llm', - config: { - name: 'Product Recommender', // REQUIRED - // Model is configured globally via OPENAI_MODEL (required; no default model is provided) - temperature: 0.3, // 0.0-1.0 (lower = more deterministic) - maxTokens: 2000, - maxSteps: 10, // For tool-using LLMs - outputFormat: 'json', // or 'text' - tools: ['customer_search', 'product_get', 'list_products'], - systemPrompt: 'You are an expert product recommender...', // REQUIRED - userPrompt: 'Customer: {{customerName}}\\nGenerate 5 recommendations.', // OPTIONAL but recommended - }, -} -``` - -**Key Points**: - -- `name` and `systemPrompt` are REQUIRED -- Model selection: The model is configured globally via the `OPENAI_MODEL` environment variable (required; no default) and is not set per step. -- Use `systemPrompt` for role/instructions -- Use `userPrompt` for specific task with context -- `outputFormat: 'json'` requires LLM to return valid JSON -- `tools` array enables LLM to call tools for data fetching - -### Approval Creation - -**Create Approval Record:** - -```typescript -{ - stepType: 'action', - config: { - type: 'approval', - parameters: { - operation: 'create_approval', - organizationId: '{{organizationId}}', - resourceType: 'conversations', // or 'product_recommendation' - resourceId: '{{conversationId}}', - priority: 'medium', // 'low', 'medium', 'high' - description: 'Review AI-generated response', - metadata: { - emailBody: '{{steps.generate_content.output.data}}', - customerId: '{{customerId}}', - }, - }, - }, -} -``` - -### Conditional Create/Update - -**Query → Check → Create or Update:** - -```typescript -// Step 1: Query existing -{ - stepSlug: 'query_entity', - stepType: 'action', - config: { - type: 'product', - parameters: { - operation: 'query', - organizationId: '{{organizationId}}', - externalId: '{{externalId}}', - }, - }, - nextSteps: { success: 'check_exists' }, -} - -// Step 2: Check if exists -{ - stepSlug: 'check_exists', - stepType: 'condition', - config: { - expression: 'steps.query_entity.output.data.count > 0', - }, - nextSteps: { - true: 'update_entity', - false: 'create_entity', - }, -} - -// Step 3a: Update -{ - stepSlug: 'update_entity', - stepType: 'action', - config: { - type: 'product', - parameters: { - operation: 'update', - externalId: '{{externalId}}', - updates: { /* ... */ }, - }, - }, - nextSteps: { success: 'next_step' }, -} - -// Step 3b: Create -{ - stepSlug: 'create_entity', - stepType: 'action', - config: { - type: 'product', - parameters: { - operation: 'create', - /* ... */ - }, - }, - nextSteps: { success: 'next_step' }, -} -``` - ---- - -## Best Practices - -### 1. Naming Conventions - -- **stepSlug**: Use snake_case (e.g., `find_unprocessed_customer`) -- **name**: Use Title Case (e.g., `Find Unprocessed Customer`) -- **workflowId**: Use kebab-case (e.g., `customer-status-assessment`) -- **Variables**: Use camelCase (e.g., `currentCustomerId`) - -### 2. Entity Processing - -- **Always** use scheduled trigger for entity processing workflows -- **Always** use workflow_processing_records to track processed entities -- **Always** set appropriate backoffHours (e.g., 168 for 7 days) -- **Always** record entity as processed at the end -- Process ONE entity per execution - -### 3. Error Handling - -- Use `retryPolicy` in workflowConfig for transient failures -- Use condition steps to handle expected failure cases -- Use `noop` in nextSteps to gracefully end workflow -- Add descriptive error messages in metadata - -### 4. Variable Management - -- Extract complex data early with `set_variables` -- Use descriptive variable names -- Store sensitive data in secrets namespace with `secure: true` -- Access secrets with `{{secrets.variableName}}` - -### 5. LLM Best Practices - -- Use low temperature (0.0-0.3) for factual/analytical tasks -- Use higher temperature (0.7-1.0) for creative tasks -- Always specify `outputFormat` ('json' or 'text') -- Provide clear, detailed system prompts -- Use `maxSteps` when enabling tools -- Validate LLM output with condition steps - -### 6. Performance - -- Use indexes for database queries (defined in schema) -- Avoid nested loops when possible -- Use pagination for large datasets -- Set reasonable timeout values -- Use backoff periods to avoid overwhelming systems - -### 7. Documentation - -- Add comprehensive workflow description -- Document key features and flow -- Explain configuration variables -- Provide examples of expected inputs/outputs - -### 8. Flow Control - -- Use `noop` to end workflow gracefully -- Always define all possible nextSteps branches -- Avoid infinite loops (use counters and limits) -- Use condition steps for branching logic -- Test all branches thoroughly - ---- - -## Complete Example: Customer Status Assessment - -```typescript -import type { InlineWorkflowDefinition } from '../workflow/types/inline'; - -export const customerStatusWorkflow: InlineWorkflowDefinition = { - workflowConfig: { - name: 'Customer Status Assessment', - description: 'Analyze customer status based on subscription data', - workflowType: 'predefined', - version: '1.0.0', - config: { - timeout: 120000, - retryPolicy: { maxRetries: 2, backoffMs: 1000 }, - variables: { - organizationId: 'org_demo', - workflowId: 'assess-customer-status', - backoffHours: 72, - }, - }, - }, - stepsConfig: [ - { - stepSlug: 'start', - name: 'Start', - stepType: 'trigger', - order: 1, - config: { - type: 'schedule', - schedule: '0 */2 * * *', - timezone: 'UTC', - }, - nextSteps: { success: 'find_customer' }, - }, - { - stepSlug: 'find_customer', - name: 'Find Unprocessed Customer', - stepType: 'action', - order: 2, - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'find_unprocessed', - organizationId: '{{organizationId}}', - tableName: 'customers', - workflowId: '{{workflowId}}', - backoffHours: '{{backoffHours}}', - }, - }, - nextSteps: { success: 'check_found' }, - }, - { - stepSlug: 'check_found', - name: 'Check if Customer Found', - stepType: 'condition', - order: 3, - config: { - expression: 'steps.find_customer.output.data.count > 0', - }, - nextSteps: { - true: 'extract_data', - false: 'noop', - }, - }, - { - stepSlug: 'extract_data', - name: 'Extract Customer Data', - stepType: 'action', - order: 4, - config: { - type: 'set_variables', - parameters: { - variables: [ - { - name: 'customerId', - value: '{{steps.find_customer.output.data.documents[0]._id}}', - }, - { - name: 'customerName', - value: '{{steps.find_customer.output.data.documents[0].name}}', - }, - ], - }, - }, - nextSteps: { success: 'analyze_status' }, - }, - { - stepSlug: 'analyze_status', - name: 'Analyze Status', - stepType: 'llm', - order: 5, - config: { - name: 'Status Analyzer', - model: 'gpt-4o', - temperature: 0.3, - maxTokens: 500, - outputFormat: 'json', - systemPrompt: - 'Analyze customer status. Return JSON: {"status": "active|churned|potential"}', - userPrompt: 'Customer: {{customerName}}', - }, - nextSteps: { success: 'update_status' }, - }, - { - stepSlug: 'update_status', - name: 'Update Customer Status', - stepType: 'action', - order: 6, - config: { - type: 'customer', - parameters: { - operation: 'update', - customerId: '{{customerId}}', - updates: { - status: '{{steps.analyze_status.output.data.status}}', - }, - }, - }, - nextSteps: { success: 'record_processed' }, - }, - { - stepSlug: 'record_processed', - name: 'Record as Processed', - stepType: 'action', - order: 7, - config: { - type: 'workflow_processing_records', - parameters: { - operation: 'record_processed', - organizationId: '{{organizationId}}', - tableName: 'customers', - workflowId: '{{workflowId}}', - documentId: '{{customerId}}', - documentCreationTime: - '{{steps.find_customer.output.data.documents[0]._creationTime}}', - metadata: { processedAt: '{{now}}' }, - }, - }, - nextSteps: { success: 'noop' }, - }, - ], -}; -``` - -This example demonstrates all key patterns: - -- Scheduled trigger for automated processing -- workflow_processing_records for tracking -- Condition-based branching -- Variable extraction -- LLM integration -- Entity update -- Graceful termination diff --git a/docs/workflows/workflow-types.md b/docs/workflows/workflow-types.md deleted file mode 100644 index 65b8511420..0000000000 --- a/docs/workflows/workflow-types.md +++ /dev/null @@ -1,385 +0,0 @@ -# Workflow Types - -## Overview - -The workflow system currently supports one active type of workflow: - -1. **Predefined Workflows** - Developer-defined workflows for platform integrations and data operations - -A previous design also included **Dynamic Orchestration Workflows** (user-defined or AI-generated workflows for agent collaboration). That feature has been removed from the codebase; the remaining references below are kept only for historical context and should not be treated as current behavior. - -## Important: Pure Agent Orchestration vs Hybrid Workflows - -### Pure Agent Orchestration Workflows - -**Structure:** - -- **1 Trigger node** (defines when to run) -- **Multiple LLM nodes** (each representing a different agent role) -- **No action/condition/loop nodes** (agents handle all logic) - -**Characteristics:** - -- All LLM nodes share the same threadId -- Each agent builds on previous agents' insights -- Focus on ONE object per execution (e.g., one customer) -- Agents collaborate in sequence like a team - -**Example:** Single Customer Churn Analysis - -``` -Trigger → Data Collector Agent → Behavior Analyst Agent → Churn Predictor Agent → Action Recommender Agent -``` - -### Hybrid Workflows (Currently Marked as dynamic_orchestration) - -**Structure:** - -- Mix of trigger, action, condition, loop, and LLM nodes -- May process multiple objects (pagination, batching) -- LLM nodes used for specific analysis tasks - -**Note:** These are currently marked as `dynamic_orchestration` but should potentially be reclassified as a third type or remain as `dynamic_orchestration` with the understanding that they're not "pure" dynamic orchestration. - -**Examples:** - -- `assess-customer-status` (loops through customers, uses LLM for status assessment) -- `product-recommendation` (loops through customers, uses LLM for recommendations) - -## 1. Predefined Workflows - -### Characteristics - -- **Predefined by developers** - Users cannot create their own predefined workflows -- **User provides credentials** - Users can choose which ones to use by providing their API credentials -- **Can include any node types** - Can use action, LLM, condition, loop, and other node types -- **No shared thread** - Each LLM step creates its own thread (no shared context) - -### Available Predefined Workflows - -The following predefined workflows are available: - -- `shopify-sync-products` - Synchronize products from Shopify -- `shopify-sync-customers` - Synchronize customers from Shopify -- `circuly-sync-customers` - Synchronize customers from Circuly -- `circuly-sync-products` - Synchronize products from Circuly -- `circuly-sync-subscriptions` - Synchronize subscriptions from Circuly - -### Example - -```typescript -export const shopifySyncProductsWorkflow: InlineWorkflowConfig = { - name: 'Shopify Products Sync', - description: - 'Synchronize products from Shopify to local database with pagination', - version: '2.0.0', - workflowType: 'predefined', // Predefined workflow - config: { - timeout: 300000, - retryPolicy: { maxRetries: 3, backoffMs: 2000 }, - variables: { - organizationId: 'org_demo', - shopifyDomain: 'example.myshopify.com', - pageSize: 50, - }, - secrets: { - shopifyAccessToken: { - kind: 'inlineEncrypted', - cipherText: '...', - }, - }, - }, -}; -``` - -## 2. (Legacy) Dynamic Orchestration Workflows - -> **Note:** This section describes a previous design for dynamic orchestration workflows that is no longer active in the codebase. It is retained only for historical reference. - -### Characteristics (legacy) - -- **User-defined or AI-generated** - Can be created by users or generated through conversation -- **Shared thread** - All LLM steps within the same workflow execution share a single threadId from the Convex agent module -- **Focus on single object** - Each execution focuses on a single object (e.g., checking if one specific customer is churned) -- **Query and update one record** - Designed to query and update just that one record -- **Trigger + Agent nodes** - Usually has a trigger node (defines when to run) and multiple agent nodes, each with its own prompt, tool, and model - -### Thread Management - -For dynamic orchestration workflows: - -1. A thread is created at workflow start using `components.agent.threads.createThread` -2. The threadId is stored in the `wfExecutions.threadId` field -3. All LLM steps reuse this shared threadId instead of creating new threads -4. This allows agents to maintain conversation context across steps - -### Example: Single Customer Status Assessment - -This is a pure agent orchestration workflow with only trigger and LLM nodes: - -```typescript -export default { - workflowConfig: { - name: 'Single Customer Status Assessment', - description: - 'Analyze a single customer using AI agents to determine and update their status', - workflowType: 'dynamic_orchestration' as const, - config: { - timeout: 120000, - retryPolicy: { maxRetries: 2, backoffMs: 1000 }, - variables: { - organizationId: 'org_demo', - // customerId will be provided via input - }, - }, - }, - stepsConfig: [ - // Step 1: Trigger - { - stepSlug: 'start', - name: 'Manual Trigger', - stepType: 'trigger', - order: 1, - config: { - type: 'manual', - data: { - customerId: '{{input.customerId}}', - }, - }, - nextSteps: { default: 'data_collector_agent' }, - }, - - // Step 2: Data Collector Agent (LLM) - { - stepSlug: 'data_collector_agent', - name: 'Data Collector Agent', - stepType: 'llm', - order: 2, - config: { - llmNode: { - name: 'data_collector', - model: 'gpt-4o-mini', - systemPrompt: 'You are a Data Collector Agent...', - userPrompt: - 'Gather all data for customer ID: {{trigger.data.customerId}}', - tools: ['customer_search'], - }, - }, - nextSteps: { success: 'status_analyzer_agent' }, - }, - - // Step 3: Status Analyzer Agent (LLM) - { - stepSlug: 'status_analyzer_agent', - name: 'Status Analyzer Agent', - stepType: 'llm', - order: 3, - config: { - llmNode: { - name: 'status_analyzer', - model: 'gpt-4o-mini', - systemPrompt: 'You are a Status Analyzer Agent...', - userPrompt: - 'Based on the customer data from the Data Collector Agent, analyze and determine status.', - tools: [], - }, - }, - nextSteps: { success: 'update_executor_agent' }, - }, - - // Step 4: Update Executor Agent (LLM) - { - stepSlug: 'update_executor_agent', - name: 'Update Executor Agent', - stepType: 'llm', - order: 4, - config: { - llmNode: { - name: 'update_executor', - model: 'gpt-4o-mini', - systemPrompt: 'You are an Update Executor Agent...', - userPrompt: - 'Based on the previous analysis, execute the database update using customer_update tool.', - tools: ['customer_update'], - }, - }, - nextSteps: { success: 'finish' }, - }, - - // Step 5: Finish - { - stepSlug: 'finish', - name: 'Complete Analysis', - stepType: 'trigger', - order: 5, - config: { type: 'manual' }, - nextSteps: {}, - }, - ], -}; -``` - -**Key Points:** - -- Only 5 steps: 1 trigger + 3 LLM agents + 1 finish trigger -- No action, condition, or loop nodes -- Each agent has a specific role and builds on previous agents' outputs -- All agents share the same threadId for conversation context - -**Important: Shared Thread Context** - -Since all agents share the same threadId, they can access previous agents' outputs through the conversation history. This means: - -- ❌ **No need to explicitly pass data** like `{{steps.data_collector_agent.output.data}}` -- ✅ **Agents can reference previous context** by mentioning the previous agent's role -- ✅ **Simpler prompts** - just instruct the agent to "use the data from the Data Collector Agent" -- ✅ **Natural conversation flow** - agents collaborate like a team discussing the same case - -Example: - -```typescript -// ❌ Old way (unnecessary with shared thread): -userPrompt: 'Analyze this data: {{steps.data_collector_agent.output.data}}'; - -// ✅ New way (leverages shared thread): -userPrompt: 'Based on the customer data from the Data Collector Agent, analyze and determine status.'; -``` - -## Schema Changes - -### wfDefinitions Table - -Added `workflowType` field: - -```typescript -workflowType: v.literal('predefined'); -``` - -> **Note:** Earlier drafts also supported a `'dynamic_orchestration'` workflowType. That path has been removed from the current implementation; only `'predefined'` workflows are supported now. - -### wfExecutions Table - -Added `threadId` field for agent orchestration workflows: - -```typescript -threadId: v.optional(v.string()); // Shared thread for agent orchestration workflows -``` - -## Implementation Details - -### Thread Creation and Reuse - -Thread creation and reuse now happens inside the LLM node execution layer: - -- `execute_llm_node.ts` passes an optional `threadId` into the LLM executor. -- `execute_agent_with_tools.ts` either reuses this `threadId` or creates a new thread via `components.agent.threads.createThread`. - -Example (from `execute_agent_with_tools.ts`): - -```typescript -// Reuse existing threadId when provided, otherwise create a new one -let threadId: string; -if (_args.threadId) { - // Reuse shared thread when threadId is provided - threadId = _args.threadId; - console.log('[executeAgentWithTools] Reusing shared thread', { threadId }); -} else { - // Workflows without a threadId (e.g., data sync or standalone LLM) create a new thread - const thread = await ctx.runMutation(components.agent.threads.createThread, { - title: `workflow:${config.name || 'LLM'}`, - }); - threadId = thread._id as string; - console.log('[executeAgentWithTools] Created new thread', { threadId }); -} -``` - -This replaces the older implementation that created the thread in `execute_workflow_start.ts`. - -### Thread Reuse in LLM Nodes - -LLM nodes check for existing threadId in `execute_agent_with_tools.ts`: - -```typescript -// Reuse existing threadId for agent orchestration workflows, or create a new one -let threadId: string; -if (_args.threadId) { - // Agent orchestration workflow - reuse shared thread - threadId = _args.threadId; - console.log('[executeAgentWithTools] Reusing shared thread', { threadId }); -} else { - // Data sync workflow or standalone LLM step - create new thread - const thread = await ctx.runMutation(components.agent.threads.createThread, { - title: `workflow:${config.name || 'LLM'}`, - }); - threadId = thread._id as string; - console.log('[executeAgentWithTools] Created new thread', { threadId }); -} -``` - -## Helper Functions - -### Workflow Type Helpers - -Located in `convex/workflow/types/workflow_types.ts`: - -```typescript -// Check if a workflow is a predefined workflow -export function isPredefinedWorkflow(workflowType: WorkflowType): boolean { - return workflowType === WORKFLOW_TYPE_PREDEFINED; -} - -// Check if a workflow is a dynamic orchestration workflow -export function isDynamicOrchestrationWorkflow( - workflowType: WorkflowType, -): boolean { - return workflowType === WORKFLOW_TYPE_DYNAMIC_ORCHESTRATION; -} - -// Check if a workflow key is a predefined workflow -export function isPredefinedWorkflowName(workflowKey: string): boolean { - return PREDEFINED_WORKFLOWS.includes(workflowKey as PredefinedWorkflow); -} - -// Validate that a workflow type matches its key -export function validateWorkflowTypeAndKey( - workflowType: WorkflowType, - workflowKey: string, -): { valid: boolean; error?: string }; -``` - -## Migration Guide - -### For Existing Workflows - -All existing workflow definitions have been updated to include the `workflowType` field: - -- **Predefined workflows**: `shopify-sync-*`, `circuly-sync-*`, `email-sync-*` → `workflowType: 'predefined'` -- **Dynamic orchestration workflows**: `assess-customer-status`, `product-recommendation`, etc. → `workflowType: 'dynamic_orchestration'` - -### For New Workflows - -When creating a new workflow, specify the `workflowType`: - -```typescript -export const myWorkflow: InlineWorkflowConfig = { - name: 'My Workflow', - description: 'Description', - version: '1.0.0', - workflowType: 'predefined', - config: { - // ... - }, -}; -``` - -### Default Behavior - -If `workflowType` is not specified, it defaults to `'predefined'` (dynamic orchestration workflows are no longer supported). - -## Benefits - -1. **Clear separation of concerns** - Predefined and dynamic orchestration workflows are fundamentally different -2. **Better thread management** - Dynamic orchestration workflows maintain conversation context -3. **Improved reliability** - Predefined workflows don't need thread management overhead -4. **User experience** - Users can easily identify which workflows they can customize -5. **Scalability** - Clear boundaries make it easier to optimize each type independently diff --git a/services/platform/convex/lib/variables/DESIGN.md b/services/platform/convex/lib/variables/DESIGN.md deleted file mode 100644 index 9c9b2971ec..0000000000 --- a/services/platform/convex/lib/variables/DESIGN.md +++ /dev/null @@ -1,528 +0,0 @@ -# Variables Replacement System Design - -## Overview - -The variables replacement system provides a robust, two-stage template processing pipeline that combines **Mustache** for template parsing and **JEXL** for expression evaluation. This design ensures safe, reliable variable substitution in workflow templates. - -## Architecture - -### Two-Stage Pipeline - -``` -Template String - ↓ -[Stage 1: Mustache Parser] - ↓ -Token Stream (text + expressions) - ↓ -[Stage 2: JEXL Evaluator] - ↓ -Rendered String -``` - -## Components - -### 1. Mustache Parser (`replace_variables_in_string.ts` and `replace_variables.ts`) - -**Purpose**: Parse template strings into structured tokens - -**Key Features**: - -- Tokenizes templates using `Mustache.parse()` -- Separates plain text from variable references -- Handles edge cases: nested braces, escaping, complex syntax -- Robust and battle-tested (Mustache is a mature library) - -**Token Types**: - -- `text`: Plain text content (passed through as-is) -- `name`: Variable reference `{{var}}` -- `&`: Unescaped variable reference `{{{var}}}` - -**Example**: - -```typescript -// Input: "Hello {{name}}, you have {{count}} items" -// Tokens: -// [['text', 'Hello '], ['name', 'name'], ['text', ', you have '], ['name', 'count'], ['text', ' items']] -``` - -### 2. JEXL Evaluator (`evaluate_expression.ts`) - -**Purpose**: Safely evaluate JavaScript-like expressions - -**Key Features**: - -- Sandboxed expression evaluation (no `eval()` or `Function` constructor) -- Supports complex logic: comparisons, math, ternary operators -- Custom transforms: `upper`, `lower`, `trim`, `length`, `map`, `filter`, etc. -- Type-safe with proper error handling - -**Supported Operations**: - -```javascript -// Comparisons -'age > 18'; -'status == "active"'; - -// Logic -'active && verified'; -'age > 18 || hasPermission'; - -// Math -'price * 1.1'; -'(subtotal + tax) * quantity'; - -// Ternary -'age >= 18 ? "adult" : "minor"'; - -// Transforms -'name|upper'; -'items|length'; -'description|trim|length > 10'; - -// Array operations -'items[0].price > 100'; -'["active", "pending"].includes(status)'; -``` - -### 3. JEXL Instance (`jexl_instance.ts`) - -**Purpose**: Configure JEXL with custom transforms for workflow use cases - -**Custom Transforms**: - -- **String**: `upper`, `lower`, `trim`, `length` -- **Type Conversion**: `string`, `number`, `boolean` -- **Array Operations**: `first`, `last`, `join`, `map`, `filter`, `unique`, `concat`, `find`, `sort`, `reverse`, `slice` -- **Advanced**: `parseJSON`, `formatList`, `hasOverlap` - -### 4. Context Builder (`build_context.ts`) - -**Purpose**: Prepare evaluation context from variables - -**Features**: - -- Merges nested variables -- Exposes workflow-level variables at top-level (backward compatibility) -- Adds built-in values like `now` (current ISO timestamp) - -### 5. Template Validator (`validate_template.ts`) - -**Purpose**: Validate template syntax before execution - -**Features**: - -- Extracts all `{{...}}` expressions -- Validates each expression by compiling with JEXL -- Catches syntax errors early - -## Simplified Design - -The system was simplified by **removing the `normalizeStepSyntax` function** which added unnecessary complexity: - -**Before**: Users had to choose between two syntaxes: - -- `{{ step "stepName" "property" }}` (confusing, non-standard) -- `{{ steps.stepName.property }}` (standard dot notation) - -**After**: Only one clear syntax: - -- `{{ steps.stepName.property }}` (standard dot notation) - -This eliminates confusion and makes the API more intuitive. - -## Why Both Mustache and JEXL? - -The system uses **both** libraries because each solves a different hard problem: - -1. **Mustache** solves the **parsing problem**: Finding expression boundaries in templates -2. **JEXL** solves the **evaluation problem**: Understanding and executing complex logic - -### The Division of Labor - -``` -Template: "Status: {{status == 'active' ? 'Online' : 'Offline'}}" - ↓ -Mustache: "I found an expression between {{ and }}: status == 'active' ? 'Online' : 'Offline'" - ↓ -JEXL: "I can evaluate that! status is 'active', so the result is 'Online'" - ↓ -Result: "Status: Online" -``` - -### Mustache Strengths (Parsing) - -- ✅ **Robust template parsing** - Handles complex template structures -- ✅ **Edge case handling** - Nested braces, quotes, escaping, special characters -- ✅ **Battle-tested** - Mature library used in production for years -- ✅ **Structured output** - Produces clean token stream for processing - -**Example of what Mustache handles well:** - -```typescript -// Complex template with nested braces and quotes -"User {{user.name}} said: \"{{message}}\" at {{time}}"; - -// Mustache correctly identifies 3 separate expressions: -// 1. user.name -// 2. message -// 3. time -``` - -### JEXL Strengths (Evaluation) - -- ✅ **Powerful expression evaluation** - Comparisons, math, logic, ternary operators -- ✅ **Safe sandboxed execution** - No `eval()` or arbitrary code execution -- ✅ **Extensible** - Custom transforms for domain-specific operations -- ✅ **Type-aware** - Handles numbers, booleans, arrays, objects correctly - -**Example of what JEXL handles well:** - -```typescript -// Complex expression with logic and transforms -"items|filter('status', 'active')|map('price')|sum() > 1000"; - -// JEXL understands: -// - Chained transforms (filter → map → sum) -// - Comparison operators (>) -// - Array operations -// - Property access -``` - ---- - -## Why Not Just One Library? - -### ❌ Option 1: JEXL Only (No Mustache) - -**Problem**: JEXL doesn't have a template parser. You'd need to write one yourself. - -**What you'd have to build:** - -```typescript -// You'd need to write regex to find {{ }} boundaries -function parseTemplate(template: string) { - // ❌ This is harder than it looks! - const regex = /\{\{([^}]+)\}\}/g; - - // What about these edge cases? - // "{{user.name}}" ✅ Works - // "{{status == 'active' ? 'yes' : 'no'}}" ❌ Breaks (quotes inside) - // "{{items[0].name}}" ✅ Works - // "{{obj.nested.deep}}" ✅ Works - // "Price: ${{price * 1.1}}" ✅ Works - // "{{a}} and {{b}}" ✅ Works - // "{{a ? '{{nested}}' : 'no'}}" ❌ Breaks (nested braces) - // "{{text|replace('}', ')')}}" ❌ Breaks (} inside expression) -} -``` - -**Why this is hard:** - -1. **Nested braces**: `{{a ? '{{nested}}' : 'no'}}` - Where does the expression end? -2. **Quotes**: `{{status == 'active' ? 'yes' : 'no'}}` - Don't split on `}}` inside quotes -3. **Escaping**: `{{text|replace('}', ')')}}` - Handle escaped characters -4. **Multiple expressions**: `{{a}} and {{b}}` - Track state between expressions - -**Result**: You'd spend weeks building and debugging a template parser, duplicating Mustache's work. - ---- - -### ❌ Option 2: Mustache Only (No JEXL) - -**Problem**: Mustache is **logic-less** by design. It cannot evaluate expressions. - -**What would happen:** - -```typescript -// Simple variable - ✅ Works -replaceVariables('Hello {{name}}', { name: 'Alice' }); -// Result: "Hello Alice" - -// Comparison - ❌ Fails (renders as empty string) -replaceVariables('{{age > 18}}', { age: 25 }); -// Result: "" (empty string, not "true") - -// Ternary - ❌ Fails (renders as empty string) -replaceVariables("{{age >= 18 ? 'adult' : 'minor'}}", { age: 25 }); -// Result: "" (empty string, not "adult") - -// Math - ❌ Fails (renders as empty string) -replaceVariables('{{price * 1.1}}', { price: 100 }); -// Result: "" (empty string, not "110") - -// Transforms - ❌ Fails (no custom transforms) -replaceVariables('{{name|upper}}', { name: 'alice' }); -// Result: "" (empty string, not "ALICE") - -// Array operations - ❌ Fails -replaceVariables('{{items|length}}', { items: [1, 2, 3] }); -// Result: "" (empty string, not "3") -``` - -**Why Mustache can't do this:** - -Mustache is designed to be **logic-less**. It only supports: - -- Simple variable substitution: `{{name}}` -- Sections (loops): `{{#items}}...{{/items}}` -- Conditionals (existence checks): `{{#exists}}...{{/exists}}` -- Partials (includes): `{{> partial}}` - -It **does not** support: - -- Operators: `>`, `<`, `==`, `+`, `-`, `*`, `/` -- Logic: `&&`, `||`, `!` -- Ternary: `? :` -- Custom functions/transforms -- Complex expressions - -**Result**: Your workflow templates would be severely limited. No conditional logic, no calculations, no data transformations. - ---- - -## The Solution: Mustache + JEXL - -By combining both libraries, we get: - -| Feature | Mustache Only | JEXL Only | **Mustache + JEXL** | -| -------------------- | ------------- | ---------------------- | ------------------- | -| Parse templates | ✅ | ❌ (need custom) | ✅ | -| Handle edge cases | ✅ | ❌ (need custom) | ✅ | -| Simple variables | ✅ | ✅ | ✅ | -| Comparisons | ❌ | ✅ | ✅ | -| Math operations | ❌ | ✅ | ✅ | -| Ternary operators | ❌ | ✅ | ✅ | -| Custom transforms | ❌ | ✅ | ✅ | -| Safe execution | ✅ | ✅ | ✅ | -| **Development time** | Fast | Slow (build parser) | **Fast** | -| **Maintenance** | Easy | Hard (maintain parser) | **Easy** | - -### Real-World Example - -**Workflow template:** - -```typescript -'Customer {{customer.name}} ({{customer.email}}) ordered {{items|length}} items. ' + - "Total: ${{items|map('price')|sum()|number}}. " + - "Status: {{total > 100 ? 'VIP' : 'Standard'}}"; -``` - -**How it's processed:** - -1. **Mustache** parses and finds 6 expressions: - - `customer.name` - - `customer.email` - - `items|length` - - `items|map('price')|sum()|number` - - `total > 100 ? 'VIP' : 'Standard'` - -2. **JEXL** evaluates each expression: - - `customer.name` → `"John Doe"` - - `customer.email` → `"john@example.com"` - - `items|length` → `3` - - `items|map('price')|sum()|number` → `250.50` - - `total > 100 ? 'VIP' : 'Standard'` → `"VIP"` - -3. **Result:** - ``` - "Customer John Doe (john@example.com) ordered 3 items. Total: $250.50. Status: VIP" - ``` - -**Without Mustache**: You'd have to write a custom parser to extract those 6 expressions. - -**Without JEXL**: The complex expressions would render as empty strings, making the template useless. - ---- - -## Summary - -**We need both because:** - -1. **Mustache** is the best tool for **parsing templates** (finding `{{...}}` boundaries) -2. **JEXL** is the best tool for **evaluating expressions** (understanding complex logic) -3. Building a custom parser would take weeks and be error-prone -4. Using Mustache alone would severely limit template capabilities -5. Together, they provide a robust, powerful, and maintainable solution - -**The key insight**: Don't solve problems that are already solved. Use the right tool for each job. - -## Usage Examples - -### Simple Variable Substitution - -```typescript -replaceVariables('Hello {{name}}', { name: 'World' }); -// Returns: "Hello World" -``` - -### Single Expression (Type Preserved) - -```typescript -replaceVariables('{{user.age}}', { user: { age: 25 } }); -// Returns: 25 (number, not string) -``` - -### Complex Expression - -```typescript -replaceVariables("{{age >= 18 ? 'adult' : 'minor'}}", { age: 25 }); -// Returns: "adult" -``` - -### Mixed Content - -```typescript -replaceVariables('Product: {{product.name}}, Price: {{product.price|number}}', { - product: { name: 'Widget', price: '99.99' }, -}); -// Returns: "Product: Widget, Price: 99.99" -``` - -### Nested Objects and Arrays - -```typescript -replaceVariables("{{items|map('name')|join(', ')}}", { - items: [{ name: 'A' }, { name: 'B' }], -}); -// Returns: "A, B" -``` - -## Data Flow - -``` -Input: Template + Variables - ↓ -buildContext() - ↓ -Mustache.parse() - ↓ -For each token: - - If text: append as-is - - If expression: evaluateExpression() with JEXL - ↓ -Output: Rendered string -``` - -## How JEXL Processes the Token Stream - -JEXL doesn't process the entire token stream at once. Instead, it processes **individual expression tokens** extracted by Mustache. - -### Step-by-Step Example - -**Input template:** - -``` -"Hello {{name}}, you have {{count}} items" -``` - -**Step 1: Mustache.parse() creates tokens:** - -```typescript -[ - ['text', 'Hello '], - ['name', 'name'], - ['text', ', you have '], - ['name', 'count'], - ['text', ' items'], -]; -``` - -**Step 2: Loop through tokens:** - -For each token: - -- **Token 1** `['text', 'Hello ']` → Type is 'text' → Append directly: `result = "Hello "` -- **Token 2** `['name', 'name']` → Type is 'name' → **Pass to JEXL** - - Extract expression: `"name"` - - JEXL evaluates: `evaluateExpression("name", context)` where `context = { name: "Alice", count: 5 }` - - Result: `"Alice"` - - Append: `result = "Hello Alice"` -- **Token 3** `['text', ', you have ']` → Type is 'text' → Append directly: `result = "Hello Alice, you have "` -- **Token 4** `['name', 'count']` → Type is 'name' → **Pass to JEXL** - - Extract expression: `"count"` - - JEXL evaluates: `evaluateExpression("count", context)` - - Result: `5` (number) - - Convert to string and append: `result = "Hello Alice, you have 5"` -- **Token 5** `['text', ' items']` → Type is 'text' → Append directly: `result = "Hello Alice, you have 5 items"` - -**Final output:** `"Hello Alice, you have 5 items"` - -### Complex Expression Example - -**Input template:** - -``` -"Status: {{status == 'active' ? 'Online' : 'Offline'}}" -``` - -**Step 1: Mustache tokenizes (but can't evaluate):** - -```typescript -[ - ['text', 'Status: '], - ['name', "status == 'active' ? 'Online' : 'Offline'"], // Mustache extracts this but can't evaluate it -]; -``` - -> **Important**: Mustache alone would render this as `"Status: "` (empty) because it doesn't understand the ternary operator. This is why we need JEXL! - -**Step 2: Our code processes tokens:** - -- **Token 1** `['text', 'Status: ']` → Append directly: `result = "Status: "` -- **Token 2** `['name', "status == 'active' ? 'Online' : 'Offline'"]` → **Pass to JEXL** - - Mustache extracted the expression string - - JEXL evaluates: `evaluateExpression("status == 'active' ? 'Online' : 'Offline'", { status: "active" })` - - JEXL understands comparisons, ternary operators, etc. - - Result: `"Online"` - - Append: `result = "Status: Online"` - -**Final output:** `"Status: Online"` - -**Why this works:** - -- ✅ Mustache: Robust parsing, finds the expression boundaries -- ✅ JEXL: Powerful evaluation, understands complex logic -- ✅ Together: Best of both worlds! - -### Key Points - -1. **Mustache's job**: Identify WHERE expressions are (between `{{` and `}}`) -2. **JEXL's job**: Evaluate WHAT the expressions mean -3. **Token types**: - - `'text'`: Plain text → pass through unchanged - - `'name'`: Variable/expression → evaluate with JEXL - - `'&'`: Unescaped variable → evaluate with JEXL - - Other types (sections, partials): Ignored - -4. **JEXL receives**: - - The expression string (e.g., `"name"`, `"count"`, `"status == 'active' ? 'Online' : 'Offline'"`) - - The context object with all available variables - - Returns the evaluated result - -5. **Type handling**: - - Strings: Appended as-is - - Numbers/Booleans: Converted to string then appended - - Objects/Arrays: JSON stringified then appended - - null/undefined: Treated as empty string - -## Error Handling - -- **Unresolved templates**: Throws error if `{{...}}` markers remain after rendering -- **Invalid expressions**: JEXL throws error during compilation/evaluation -- **Type mismatches**: Gracefully converts to string or JSON - -## Performance Considerations - -- **Parsing**: Mustache.parse() is fast and cached by Mustache -- **Evaluation**: JEXL evaluation is synchronous and efficient -- **Context building**: Shallow merge of variables -- **Overall**: Suitable for workflow execution (not real-time rendering) - -## Security - -- **No arbitrary code execution**: JEXL is sandboxed -- **No eval() or Function()**: Safe expression evaluation -- **Controlled transforms**: Only whitelisted operations available -- **Input validation**: Template syntax validated before execution From 2576d7a5561056e82720f700d532ed1d6ca7c19e Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Thu, 11 Dec 2025 09:31:02 +0800 Subject: [PATCH 2/2] docs: update README and env example for OpenRouter API --- .env.example | 12 ++++++------ README.md | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 7cf42f2f05..a3d1718e27 100644 --- a/.env.example +++ b/.env.example @@ -25,16 +25,16 @@ ENCRYPTION_SECRET_HEX=3143246f44def075d40141fb849faffcf409fbbeb7a282a3a7c2f4396f # ============================================================================ # REQUIRED: API Keys # ============================================================================ -# OpenAI-compatible provider (OpenAI or OpenRouter). Use your API key here. +# OpenRouter API configuration (recommended). Get your API key at https://openrouter.ai # EMBEDDING_DIMENSIONS controls the embedding vector size used by RAG/vector search. # When using custom embedding models or providers, you MUST set EMBEDDING_DIMENSIONS # to match your vector store's configured dimension (for example, 3072 for # text-embedding-3-large). -OPENAI_BASE_URL=https://api.openai.com/v1 -OPENAI_API_KEY=replace-with-your-key -OPENAI_MODEL=gpt-5-mini-2025-08-07 -OPENAI_CODING_MODEL=gpt-5 -OPENAI_EMBEDDING_MODEL=text-embedding-3-large +OPENAI_BASE_URL=https://openrouter.ai/api/v1 +OPENAI_API_KEY=your-openrouter-api-key +OPENAI_MODEL=x-ai/grok-4.1-fast +OPENAI_CODING_MODEL=x-ai/grok-4.1-fast +OPENAI_EMBEDDING_MODEL=openai/text-embedding-3-large # EMBEDDING_DIMENSIONS=3072 # ============================================================================ diff --git a/README.md b/README.md index 7487629439..d7f4ac0c63 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Get Tale running in 3 steps: ### 1. Prerequisites - [Docker Desktop](https://www.docker.com/products/docker-desktop) (v24+) -- [OpenAI API Key](https://platform.openai.com/api-keys) +- [OpenRouter API Key](https://openrouter.ai) ### 2. Clone & Configure @@ -21,10 +21,10 @@ cd tale cp .env.example .env ``` -Edit `.env` and add your OpenAI API key: +Edit `.env` and add your OpenRouter API key: ```bash -OPENAI_API_KEY=sk-your-key-here +OPENAI_API_KEY=your-openrouter-api-key ``` ### 3. Launch