From 51088a027c3382beb38a03896cab5485b9111c31 Mon Sep 17 00:00:00 2001 From: Tushar <47842976+tusharmagar@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:52:15 +0530 Subject: [PATCH 01/28] feat: add Eisenhower Email Organiser prebuilt card (#252) - Added new eisenhower-email-organiser.json prebuilt workflow template - Updated index.ts to include the new template in exports - Template helps users organize emails using the Eisenhower Matrix (urgent/important quadrants) --- .../eisenhower-email-organiser.json | 125 ++++++++++++++++++ apps/rowboat/app/lib/prebuilt-cards/index.ts | 2 + 2 files changed, 127 insertions(+) create mode 100644 apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json diff --git a/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json b/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json new file mode 100644 index 00000000..715d683f --- /dev/null +++ b/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json @@ -0,0 +1,125 @@ +{ + "agents": [ + { + "name": "Classification Agent", + "type": "pipeline", + "description": "Classifies a single email into one of the four Eisenhower Matrix categories.", + "disabled": false, + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nClassify the provided email into one of the four Eisenhower Matrix categories:\n- Urgent + Important: Critical, requires immediate action (e.g., legal, financial, investor, user-blocking).\n- Not Urgent + Important: High-value, strategic, should be scheduled (e.g., partnerships, key coordination).\n- Urgent + Not Important: Time-sensitive but delegable (e.g., routine ops, technical updates).\n- Not Urgent + Not Important: Low-value, noise, spam, promotions (should be labeled as 'Low Priority').\n\n---\n## āš™ļø Steps to Follow:\n1. Receive the email (subject, sender, body, etc.).\n2. Analyze the content and assign the correct category.\n3. Return the category as a string.\n\n---\n## šŸ“‹ Guidelines:\n- Use the provided definitions for each category.\n- Be accurate and consistent.\n- Do not perform any actions other than classification.", + "model": "google/gemini-2.5-flash", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "outputVisibility": "internal", + "controlType": "relinquish_to_parent", + "maxCallsPerParentAgent": 3 + }, + { + "name": "Label Agent", + "type": "pipeline", + "description": "Applies the correct Gmail label to the email based on its Eisenhower Matrix category.", + "disabled": false, + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nApply the correct Gmail label to the email based on its Eisenhower Matrix category, using the provided label IDs.\n\n---\n## āš™ļø Steps to Follow:\n1. Receive the email's message_id and its assigned category.\n2. Map the category to the correct label ID using these variables:\n - 'Important + Not Urgent': [@variable:Important + Not Urgent Label ID](#mention)\n - 'Important + Urgent': [@variable:Important + Urgent Label ID](#mention)\n - 'Not Important + Urgent': [@variable:Not Important + Urgent Label ID](#mention)\n - 'Not Important + Not Urgent': [@variable:Not Important + Not Urgent Label ID](#mention)\n3. Use [@tool:Modify email labels](#mention) to add the correct label ID to the email (add_label_ids).\n4. Return a status indicating the label was applied.\n\n---\n## šŸ“‹ Guidelines:\n- Always use the provided label IDs.\n- Always apply the correct label.\n- Do not archive or delete emails.", + "model": "google/gemini-2.5-flash", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "outputVisibility": "internal", + "controlType": "relinquish_to_parent", + "maxCallsPerParentAgent": 3 + } + ], + "prompts": [ + { + "name": "Important + Not Urgent Label ID", + "type": "base_prompt", + "prompt": "" + }, + { + "name": "Important + Urgent Label ID", + "type": "base_prompt", + "prompt": "" + }, + { + "name": "Not Important + Urgent Label ID", + "type": "base_prompt", + "prompt": "" + }, + { + "name": "Not Important + Not Urgent Label ID", + "type": "base_prompt", + "prompt": "" + }, + { + "name": "user_id", + "type": "base_prompt", + "prompt": "" + } + ], + "tools": [ + { + "name": "Modify email labels", + "description": "Adds and/or removes specified gmail labels for a message; ensure `message id` and all `label ids` are valid.", + "mockTool": false, + "parameters": { + "type": "object", + "properties": { + "add_label_ids": { + "type": "array", + "description": "Label IDs to add.", + "items": { + "type": "string" + }, + "default": [] + }, + "message_id": { + "type": "string", + "description": "Immutable ID of the message to modify." + }, + "remove_label_ids": { + "type": "array", + "description": "Label IDs to remove.", + "items": { + "type": "string" + }, + "default": [] + }, + "user_id": { + "type": "string", + "description": "User's email address or 'me' for the authenticated user.", + "default": "me" + } + }, + "required": [ + "message_id" + ] + }, + "isComposio": true, + "composioData": { + "slug": "GMAIL_ADD_LABEL_TO_EMAIL", + "noAuth": false, + "toolkitName": "gmail", + "toolkitSlug": "gmail", + "logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/gmail.svg" + } + } + ], + "pipelines": [ + { + "name": "Eisenhower Email Pipeline", + "description": "Pipeline that classifies an email and applies the correct label (including 'Low Priority' for low-value emails).", + "agents": [ + "Classification Agent", + "Label Agent" + ] + } + ], + "startAgent": "Eisenhower Email Pipeline", + "lastUpdatedAt": "2025-09-13T20:34:42.747Z", + "name": "Eisenhower Email Organiser", + "description": "Organises emails into the four Eisenhower Matrix categories.", + "category": "Work Productivity", + "copilotPrompt": "Give me a brief explanation of this assistant. Also briefly tell me about how to setup a trigger for this assistant." +} \ No newline at end of file diff --git a/apps/rowboat/app/lib/prebuilt-cards/index.ts b/apps/rowboat/app/lib/prebuilt-cards/index.ts index 10d39ee6..16f2305f 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/index.ts +++ b/apps/rowboat/app/lib/prebuilt-cards/index.ts @@ -10,6 +10,7 @@ import tweetWithGeneratedImage from './tweet-with-generated-image.json'; import customerSupport from './customer-support.json'; import githubIssueToSlack from './github-issue-to-slack.json'; import githubPrToSlack from './github-pr-to-slack.json'; +import eisenhowerEmailOrganiser from './eisenhower-email-organiser.json'; // Keep keys consistent with prior file basenames to avoid breaking links. export const prebuiltTemplates = { @@ -22,5 +23,6 @@ export const prebuiltTemplates = { 'Customer Support': customerSupport, 'GitHub Issue to Slack': githubIssueToSlack, 'GitHub PR to Slack': githubPrToSlack, + 'Eisenhower Email Organiser': eisenhowerEmailOrganiser, }; From 08c0879b789d8d0625b42b233fa4cd078cc52141 Mon Sep 17 00:00:00 2001 From: arkml Date: Mon, 15 Sep 2025 14:29:22 +0530 Subject: [PATCH 02/28] remove example box and examples from copilot (#254) * remove example box and examples from copilot * removed example field from prebuilt jsons --- .../lib/prebuilt-cards/customer-support.json | 1 - .../prebuilt-cards/interview-scheduler.json | 1 - .../[projectId]/entities/agent_config.tsx | 101 +----------------- .../lib/copilot/example_multi_agent_1.ts | 9 +- 4 files changed, 4 insertions(+), 108 deletions(-) diff --git a/apps/rowboat/app/lib/prebuilt-cards/customer-support.json b/apps/rowboat/app/lib/prebuilt-cards/customer-support.json index f60c763c..f4234f01 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/customer-support.json +++ b/apps/rowboat/app/lib/prebuilt-cards/customer-support.json @@ -5,7 +5,6 @@ "type": "conversation", "description": "Hub agent to answer product information questions (using RAG) and delivery status questions.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou are the hub agent responsible for orchestrating responses to product information and delivery status questions.\n\n---\n## āš™ļø Steps to Follow:\n1. Greet the user and ask how you can help. Say something like 'Hi, I'm [@variable:Assistant name](#mention) from [@variable:Company name](#mention). How can I help you today?'\n2. Determine if the user's question is about product information or delivery status.\n3. If the question is about product information, transfer to [@agent:Product Information Agent](#mention).\n4. If the question is about delivery status, transfer to [@agent:Delivery Status Agent](#mention).\n5. If the question is neither, politely inform the user that you can only help with product information or delivery status.\n6. Return the final answer to the user.\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Routing product information questions.\n- Routing delivery status questions.\n\nāŒ Out of Scope:\n- Directly answering product or delivery questions.\n- Handling questions outside of product information or delivery status.\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Clearly identify the type of user query.\n- Route to the correct agent.\n\n🚫 Don'ts:\n- Do not attempt to answer questions directly.\n- Do not ask for personal information unless explicitly required by a sub-agent.\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.\n\n", - "examples": "- **User** : What are the features of product X?\n - **Agent actions**: Call [@agent:Product Information Agent](#mention)\n\n- **User** : Where is my order?\n - **Agent actions**: Call [@agent:Delivery Status Agent](#mention)\n\n- **User** : How do I reset my password?\n - **Agent response**: I can only help with product information or delivery status questions. How else can I assist you today?", "model": "gpt-4o", "toggleAble": true, "ragReturnType": "chunks", diff --git a/apps/rowboat/app/lib/prebuilt-cards/interview-scheduler.json b/apps/rowboat/app/lib/prebuilt-cards/interview-scheduler.json index 3c192ac8..56c1b556 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/interview-scheduler.json +++ b/apps/rowboat/app/lib/prebuilt-cards/interview-scheduler.json @@ -6,7 +6,6 @@ "type": "conversation", "description": "Hub agent to orchestrate interview scheduling with candidates from a Google Sheet.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou are the Recruitment HR Bot, responsible for orchestrating the process of scheduling interviews with candidates from a Google Sheet and updating their status, or handling calendar event RSVPs.\n\n---\n## āš™ļø Steps to Follow:\n1. Greet the user.\n2. **IF** the input is a calendar event RSVP (e.g., 'accepted', 'declined') and contains the candidate's email, Google Sheet ID, sheet name, and status column:\n - Directly call [@agent:Calendar Response Handler](#mention) with the candidate's email, the RSVP response, the Google Sheet ID, the sheet name, and the status column.\n - Inform the user that the calendar response has been processed.\n3. **ELSE** (if it's not a calendar event RSVP or missing details for it):\n - Check if the 'google sheet id' and 'Sheet range' prompts are available. If so, use their values. Otherwise, ask the user for the Google Sheet ID and the range containing candidate names and emails (e.g., 'Sheet1!A2:B').\n - Check if the 'interview start date and time' and 'Status column' prompts are available. If so, use their values. Otherwise, ask for the desired start date and time for interviews (e.g., 'YYYY-MM-DDTHH:MM:SS'), the duration of the interview in minutes, and the sheet name and column (e.g., 'Sheet1!C') where the interview status should be updated.\n - Once all necessary information is collected, call [@pipeline:Interview Scheduling Pipeline](#mention) with the collected details.\n - Inform the user when the interview scheduling process is complete.\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Orchestrating the workflow for fetching candidates, scheduling interviews, and updating sheet status.\n- Handling calendar event RSVPs and updating sheet status accordingly.\n\nāŒ Out of Scope:\n- Directly fetching candidate data, scheduling interviews, or updating sheet status (handled by pipeline agents).\n- Directly processing calendar responses (handled by Calendar Response Handler).\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Prioritize handling calendar event RSVPs if the necessary information is present.\n- Always confirm all necessary details (Sheet ID, ranges, interview time, duration, status column) with the user before initiating the pipeline for interview scheduling.\n- Ensure all steps are completed in sequence.\n- If inputs are already in the context, directly use them instead of asking or confirming with the user.\n\n🚫 Don'ts:\n- Do not perform data fetching, scheduling, or status updates directly.\n- Do not skip any step in the workflow.\n- Do not mention internal agent names to the user.\n- Do not say 'connecting you to another agent'.\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.\n\n---\n## šŸ“„ Inputs:\n- **Google Sheet ID**: The unique identifier of the Google Spreadsheet containing candidate data. (e.g., '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms')\n- **Sheet Range**: The range in A1 notation (e.g., 'Sheet1!A2:B') containing candidate names and emails.\n- **Interview Start Date and Time**: The desired start date and time for interviews in 'YYYY-MM-DDTHH:MM:SS' format. Default: '2025-08-26T09:00:00'\n- **Interview Duration**: The duration of the interview in minutes. Default: 30\n- **Status Column**: The sheet name and column (e.g., 'Sheet1!C') where the interview status should be updated.", - "examples": "- **User** : I want to schedule interviews for candidates from a Google Sheet.\n - **Agent response**: Sure! Please provide the desired start date and time for interviews (e.g., 'YYYY-MM-DDTHH:MM:SS'), the duration of the interview in minutes, and the sheet name and column (e.g., 'Sheet1!C') where the interview status should be updated.\n\n- **User** : Start Time: 2024-09-01T10:00:00, Duration: 30, Status Column: Sheet1!C\n - **Agent actions**: Call [@pipeline:Interview Scheduling Pipeline](#mention)\n\n- **Agent receives pipeline completion** :\n - **Agent response**: The interview scheduling process is complete.\n\n- **User** : Candidate [candidate_email] has accepted the interview. Sheet ID: [sheet_id], Sheet Name: [sheet_name], Status Column: [status_column]\n - **Agent actions**: Call [@agent:Calendar Response Handler](#mention)\n\n- **Agent receives Calendar Response Handler completion** :\n - **Agent response**: The calendar response has been processed and the sheet updated.", "model": "google/gemini-2.5-flash", "toggleAble": true, "ragReturnType": "chunks", diff --git a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx index 3ad2c96a..f3cc65a0 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx @@ -68,7 +68,6 @@ export function AgentConfig({ const [isAdvancedConfigOpen, setIsAdvancedConfigOpen] = useState(false); const [showGenerateModal, setShowGenerateModal] = useState(false); const [isInstructionsMaximized, setIsInstructionsMaximized] = useState(false); - const [isExamplesMaximized, setIsExamplesMaximized] = useState(false); const { showPreview } = usePreviewModal(); const [localName, setLocalName] = useState(agent.name); const [nameError, setNameError] = useState(null); @@ -172,15 +171,12 @@ export function AgentConfig({ if (isInstructionsMaximized) { setIsInstructionsMaximized(false); } - if (isExamplesMaximized) { - setIsExamplesMaximized(false); - } } }; window.addEventListener('keydown', handleEscape); return () => window.removeEventListener('keydown', handleEscape); - }, [isInstructionsMaximized, isExamplesMaximized]); + }, [isInstructionsMaximized]); const validateName = (value: string) => { if (value.length === 0) { @@ -438,98 +434,7 @@ export function AgentConfig({ className="h-full min-h-0 overflow-auto !mb-0 !mt-0 min-h-[300px]" /> - {/* Examples Section */} -
-
- - -
- {!isExamplesMaximized && ( -
- šŸ’” Tip: Use the maximized view for a better editing experience -
- )} - {isExamplesMaximized ? ( -
-
- {/* Saved Banner for maximized examples */} - {showSavedBanner && ( -
- - - - Changes saved -
- )} -
-
- {agent.name} - / - Examples -
- -
-
- { - handleUpdate({ - ...agent, - examples: value - }); - showSavedMessage(); - }} - placeholder="Enter examples for this agent" - markdown - multiline - mentions - mentionsAtValues={atMentions} - className="h-full min-h-0 overflow-auto !mb-0 !mt-0" - /> -
-
-
- ) : ( - { - handleUpdate({ - ...agent, - examples: value - }); - showSavedMessage(); - }} - placeholder="Enter examples for this agent" - markdown - multiline - mentions - mentionsAtValues={atMentions} - className="h-full min-h-0 overflow-auto !mb-0 !mt-0" - /> - )} -
+ {/* Examples Section removed */} )} @@ -989,4 +894,4 @@ function validateAgentName(value: string, currentName?: string, usedNames?: Set< return "Name must contain only letters, numbers, underscores, hyphens, and spaces"; } return null; -} \ No newline at end of file +} diff --git a/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts b/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts index 32952654..9e7ba948 100644 --- a/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts +++ b/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts @@ -129,7 +129,6 @@ I'll create the hub agent: "type": "conversation", "description": "Hub agent to orchestrate meeting retrieval, participant research, summary generation, and email delivery.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nYou are the hub agent responsible for orchestrating the process of viewing meetings, researching participants, summarizing meetings, and sending summaries via email.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Greet the user and ask for the time period for which they want to view meetings.\\n2. Ask for the user's email address to send the summary.\\n3. Call [@agent:Meeting Fetch Agent](#mention) with the specified time period.\\n4. For each meeting returned, call [@agent:Participant Research Agent](#mention) to research all participants.\\n5. For each meeting, call [@agent:Meeting Summary Agent](#mention) to generate a summary using meeting details and participant research.\\n6. For each summary, call [@agent:Email Agent](#mention) to send the summary to the user's email.\\n7. Inform the user when all summaries have been sent.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Orchestrating the workflow for meeting retrieval, research, summary, and email delivery.\\n\\nāŒ Out of Scope:\\n- Directly fetching meetings, researching, summarizing, or sending emails (handled by sub-agents).\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Always confirm the time period and email address with the user.\\n- Ensure all steps are completed in sequence for each meeting.\\n\\n🚫 Don'ts:\\n- Do not perform research, summary, or email sending directly.\\n- Do not skip any step in the workflow.\\n- Do not mention internal agent names to the user.\\n- Do not say 'connecting you to another agent'.\\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.", - "examples": "- **User** : I want to see my meetings for next week and get summaries.\\n - **Agent response**: Sure! Please provide the start and end dates for the period you'd like to review.\\n\\n- **User** : From 2024-08-01 to 2024-08-07. My email is [USER_EMAIL]\\n - **Agent actions**: Call [@agent:Meeting Fetch Agent](#mention)\\n\\n- **Agent receives meetings** :\\n - **Agent actions**: For each meeting, call [@agent:Participant Research Agent](#mention)\\n\\n- **Agent receives participant research** :\\n - **Agent actions**: For each meeting, call [@agent:Meeting Summary Agent](#mention)\\n\\n- **Agent receives summary** :\\n - **Agent actions**: For each summary, call [@agent:Email Agent](#mention)\\n\\n- **Agent receives email confirmation** :\\n - **Agent response**: All meeting summaries have been sent to your email.", "model": "gpt-4.1", "outputVisibility": "user_facing", "controlType": "retain" @@ -150,7 +149,6 @@ I'll create the hub agent: "type": "task", "description": "Fetches meetings from Google Calendar for a specified time period.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nFetch meetings from the user's Google Calendar for the specified time period.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Receive the time period (start and end date/time) from the parent agent.\\n2. Use [@tool:Find event](#mention) to fetch all meetings in that period.\\n3. Return the list of meetings (with details: title, time, participants, description, etc.) to the parent agent.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Fetching meetings for a given time period.\\n\\nāŒ Out of Scope:\\n- Researching participants.\\n- Summarizing meetings.\\n- Sending emails.\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Return all relevant meeting details.\\n\\n🚫 Don'ts:\\n- Do not perform research or summaries.\\n- Do not interact with the user directly.", - "examples": "- **Parent agent** : Fetch meetings from 2024-08-01 to 2024-08-07.\\n - **Agent actions**: Call [@tool:Find event](#mention)\\n - **Agent response**: [List of meetings with details]", "model": "gpt-4.1", "outputVisibility": "internal", "controlType": "relinquish_to_parent" @@ -171,7 +169,6 @@ I'll create the hub agent: "type": "task", "description": "Researches each meeting participant using web search.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nResearch each participant in the meeting using web search and return a brief profile for each.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Receive a list of participant names and emails from the parent agent.\\n2. For each participant, use [@tool:Tavily search](#mention) to find relevant information.\\n3. Summarize the findings for each participant (role, company, notable info).\\n4. Return the research summaries to the parent agent.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Researching participants using web search.\\n\\nāŒ Out of Scope:\\n- Fetching meetings.\\n- Summarizing meetings.\\n- Sending emails.\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Provide concise, relevant participant profiles.\\n\\n🚫 Don'ts:\\n- Do not fabricate information.\\n- Do not interact with the user directly.", - "examples": "- **Parent agent** : Research participants: [ATTENDEE_1_NAME] ([ATTENDEE_1_EMAIL]), [ATTENDEE_2_NAME] ([ATTENDEE_2_EMAIL])\\n - **Agent actions**: Call [@tool:Tavily search](#mention) for each participant\\n - **Agent response**: [ATTENDEE_1_NAME]: [summary], [ATTENDEE_2_NAME]: [summary]", "model": "gpt-4.1", "outputVisibility": "internal", "controlType": "relinquish_to_parent" @@ -192,7 +189,6 @@ I'll create the hub agent: "type": "task", "description": "Generates a summary of the meeting using meeting details and participant research.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nGenerate a concise summary of the meeting, incorporating meeting details and participant research.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Receive meeting details and participant research from the parent agent.\\n2. Write a summary including:\\n - Meeting title, date, and time\\n - Purpose/agenda (if available)\\n - Key participants and their profiles\\n - Any notable context\\n3. Return the summary to the parent agent.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Summarizing meetings using provided details and research.\\n\\nāŒ Out of Scope:\\n- Fetching meetings.\\n- Researching participants.\\n- Sending emails.\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Be clear and concise.\\n- Highlight important details.\\n\\n🚫 Don'ts:\\n- Do not add information not provided.\\n- Do not interact with the user directly.", - "examples": "- **Parent agent** : Summarize meeting: 'Q3 Planning', 2024-08-02 10:00, participants: [Alice summary, Bob summary]\\n - **Agent response**: Meeting: Q3 Planning (2024-08-02 10:00)\\nParticipants: [ATTENDEE_1_NAME] ([ATTENDEE_1_ROLE] at [COMPANY_1]), [ATTENDEE_2_NAME] ([ATTENDEE_2_ROLE] at [COMPANY_2])\\nSummary: The meeting will focus on Q3 product roadmap and resource allocation.", "model": "gpt-4.1", "outputVisibility": "internal", "controlType": "relinquish_to_parent" @@ -213,7 +209,6 @@ I'll create the hub agent: "type": "task", "description": "Sends the meeting summary to the user's email address.", "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nSend the provided meeting summary to the user's email address.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Receive the meeting summary and recipient email from the parent agent.\\n2. Use [@tool:Send Email](#mention) to send the summary.\\n3. Confirm delivery to the parent agent.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Sending meeting summaries via email.\\n\\nāŒ Out of Scope:\\n- Fetching meetings.\\n- Researching participants.\\n- Summarizing meetings.\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Ensure the summary is sent to the correct email.\\n\\n🚫 Don'ts:\\n- Do not interact with the user directly.", - "examples": "- **Parent agent** : Send summary to [USER_EMAIL]: [summary text]\\n - **Agent actions**: Call [@tool:Send Email](#mention)\\n - **Agent response**: Email sent confirmation.", "model": "gpt-4.1", "outputVisibility": "internal", "controlType": "relinquish_to_parent" @@ -303,7 +298,7 @@ I'm creating a user-facing agent that fetches a Google Doc by ID and answers que "name": "Google Doc QnA Assistant", "type": "conversation", "description": "Answers user questions based solely on the content of a specified Google Doc.", - "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nYou are an assistant that answers user questions using only the content of a specified Google Doc.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Ask the user for the Google Doc ID and their question.\\n2. Use the [@tool:Get document by id](#mention) tool to fetch the document content.\\n3. Read the content of the document.\\n4. Answer the user's question using only the information found in the document. If the answer is not present in the document, politely inform the user that the information is not available.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Answering questions strictly based on the content of the provided Google Doc.\\n\\nāŒ Out of Scope:\\n- Answering questions not related to the content of the provided Google Doc.\\n- Using external sources or prior knowledge.\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Always fetch the document before answering.\\n- Be concise and accurate.\\n- If the answer is not in the document, say so politely.\\n\\n🚫 Don'ts:\\n- Do not use information outside the document.\\n- Do not attempt to answer unrelated questions.\\n- Do not use RAG or external search.\\n\\n# Examples\\n- **User** : What is the project deadline? The doc ID is 1A2B3C4D5E6F7G8H9I0J\\n - **Agent actions**: Call [@tool:Get document by id](#mention)\\n - **Agent response**: The project deadline is June 30, 2024. (if found in doc)\\n\\n- **User** : Who is the project manager? The doc ID is 1A2B3C4D5E6F7G8H9I0J\\n - **Agent actions**: Call [@tool:Get document by id](#mention)\\n - **Agent response**: The project manager is [PROJECT_MANAGER_NAME]. (if found in doc)\\n\\n- **User** : What is the weather today? The doc ID is 1A2B3C4D5E6F7G8H9I0J\\n - **Agent actions**: Call [@tool:Get document by id](#mention)\\n - **Agent response**: Sorry, I can only answer questions based on the content of the provided Google Doc.\\n\\n- **User** : Tell me about the budget. The doc ID is 1A2B3C4D5E6F7G8H9I0J\\n - **Agent actions**: Call [@tool:Get document by id](#mention)\\n - **Agent response**: The budget for the project is $50,000. (if found in doc)\\n\\n- **User** : Can you summarize the document? The doc ID is 1A2B3C4D5E6F7G8H9I0J\\n - **Agent actions**: Call [@tool:Get document by id](#mention)\\n - **Agent response**: [Provides a brief summary of the document's main points]", + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\\nYou are an assistant that answers user questions using only the content of a specified Google Doc.\\n\\n---\\n## āš™ļø Steps to Follow:\\n1. Ask the user for the Google Doc ID and their question.\\n2. Use the [@tool:Get document by id](#mention) tool to fetch the document content.\\n3. Read the content of the document.\\n4. Answer the user's question using only the information found in the document. If the answer is not present in the document, politely inform the user that the information is not available.\\n\\n---\\n## šŸŽÆ Scope:\\nāœ… In Scope:\\n- Answering questions strictly based on the content of the provided Google Doc.\\n\\nāŒ Out of Scope:\\n- Answering questions not related to the content of the provided Google Doc.\\n- Using external sources or prior knowledge.\\n\\n---\\n## šŸ“‹ Guidelines:\\nāœ”ļø Dos:\\n- Always fetch the document before answering.\\n- Be concise and accurate.\\n- If the answer is not in the document, say so politely.\\n\\n🚫 Don'ts:\\n- Do not use information outside the document.\\n- Do not attempt to answer unrelated questions.\\n- Do not use RAG or external search.\\n", "model": "gpt-4.1", "outputVisibility": "user_facing", "controlType": "retain" @@ -1233,7 +1228,6 @@ I'll create an agent to handle product information questions. You can later conn "description": "Answers product information questions using RAG data sources.", "disabled": false, "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou are an internal agent that answers product information questions using RAG data sources. If you receive a question that is not about product information, you must return control to the parent agent with a message indicating the question is out of your scope.\n\n---\n## āš™ļø Steps to Follow:\n1. Receive the product information question from the parent agent.\n2. Determine if the question is about product information.\n - If yes: Use RAG search to pull information from the available data sources to answer the question.\n - If not: Return control to the parent agent with a message such as \"This question is not about product information. Returning to parent agent.\"\n3. Formulate a clear and concise answer based on the RAG results (if applicable).\n4. If question is out of scope call [@agent:Product & Delivery Assistant](#mention) \n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Answering product information questions using RAG.\n- Returning control to parent if the question is out of scope.\n\nāŒ Out of Scope:\n- Handling delivery status questions.\n- Interacting directly with the user.\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Use RAG search to find relevant information for product questions.\n- If the question is not about product information, return control to the parent agent with a clear message.\n\n🚫 Don'ts:\n- Do not answer questions outside of product information.\n- Do not interact with the user directly.\n- Do not ignore out-of-scope questions; always return to parent.\n", - "examples": "\n", "model": "google/gemini-2.5-flash", "locked": false, "toggleAble": true, @@ -1262,7 +1256,6 @@ I'll create an agent to handle delivery status questions that uses a mocked tool "description": "Answers delivery status questions using the Exa Answer tool.", "disabled": false, "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou are an internal agent that answers delivery status questions. If you receive a question that is not about delivery status, you must return control to the parent agent with a message indicating the question is out of your scope.\n\n---\n## āš™ļø Steps to Follow:\n1. Receive the delivery status question from the parent agent.\n2. Determine if the question is about delivery status.\n - If yes: Use the [@tool:Mock Delivery Status](#mention) tool to search for delivery status information. You may need to ask the user for an order number or tracking ID if not provided.\n - If not: Return control to the parent agent with a message such as \"This question is not about delivery status. Returning to parent agent.\"\n3. Formulate a clear and concise answer based on the tool's results (if applicable).\n4. If question is out of scope call [@agent:Product & Delivery Assistant](#mention) \n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Answering delivery status questions using the Exa Answer tool.\n- Returning control to parent if the question is out of scope.\n\nāŒ Out of Scope:\n- Handling product information questions.\n- Interacting directly with the user (except to ask for necessary information like order ID).\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Use the Exa Answer tool to find delivery information for delivery status questions.\n- If the question is not about delivery status, return control to the parent agent with a clear message.\n- Ask for order details if needed.\n\n🚫 Don'ts:\n- Do not answer questions outside of delivery status.\n- Do not interact with the user directly unless absolutely necessary to get information for the tool.\n- Do not ignore out-of-scope questions; always return to parent.\n", - "examples": "\n", "model": "gpt-4.1", "locked": false, "toggleAble": true, From 66b6629abbeeb583ad75b9ac6e210766155ecbf2 Mon Sep 17 00:00:00 2001 From: Tushar <47842976+tusharmagar@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:31:06 +0530 Subject: [PATCH 03/28] Add eisenhower email organiser (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Eisenhower Email Organiser prebuilt card - Added new eisenhower-email-organiser.json prebuilt workflow template - Updated index.ts to include the new template in exports - Template helps users organize emails using the Eisenhower Matrix (urgent/important quadrants) * fix: update to American spelling - organizer instead of organiser - Updated import statement to use 'eisenhower-email-organizer.json' - Updated export key to 'Eisenhower Email Organizer' - Maintains consistency with American English throughout the codebase * change name of Tweet Assistant * fix: remove duplicate file and fix American spelling in description - Removed duplicate eisenhower-email-organiser.json (British spelling) - Fixed description in eisenhower-email-organizer.json: 'Organises' → 'Organizes' - Now only one file exists with consistent American spelling throughout * fix: update Support category header in prebuilt cards UI - Changed 'Customer Support' to 'Support' in build-assistant-section.tsx - Now matches the category field in customer-support.json - Ensures Support section displays properly in the prebuilt cards UI --- apps/rowboat/app/lib/prebuilt-cards/customer-support.json | 2 +- ...r-email-organiser.json => eisenhower-email-organizer.json} | 4 ++-- apps/rowboat/app/lib/prebuilt-cards/index.ts | 4 ++-- .../app/lib/prebuilt-cards/tweet-with-generated-image.json | 2 +- .../app/projects/components/build-assistant-section.tsx | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename apps/rowboat/app/lib/prebuilt-cards/{eisenhower-email-organiser.json => eisenhower-email-organizer.json} (98%) diff --git a/apps/rowboat/app/lib/prebuilt-cards/customer-support.json b/apps/rowboat/app/lib/prebuilt-cards/customer-support.json index f4234f01..dd17f608 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/customer-support.json +++ b/apps/rowboat/app/lib/prebuilt-cards/customer-support.json @@ -107,6 +107,6 @@ "lastUpdatedAt": "2025-09-11T18:51:15.548Z", "name": "Customer Support", "description": "Answers product information (RAG) and delivery status (MCP) questions.", - "category": "Customer Support", + "category": "Support", "copilotPrompt": "Give me a brief explanation of this assistant." } diff --git a/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json b/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organizer.json similarity index 98% rename from apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json rename to apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organizer.json index 715d683f..b005f498 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organiser.json +++ b/apps/rowboat/app/lib/prebuilt-cards/eisenhower-email-organizer.json @@ -118,8 +118,8 @@ ], "startAgent": "Eisenhower Email Pipeline", "lastUpdatedAt": "2025-09-13T20:34:42.747Z", - "name": "Eisenhower Email Organiser", - "description": "Organises emails into the four Eisenhower Matrix categories.", + "name": "Eisenhower Email Organizer", + "description": "Organizes emails into the four Eisenhower Matrix categories.", "category": "Work Productivity", "copilotPrompt": "Give me a brief explanation of this assistant. Also briefly tell me about how to setup a trigger for this assistant." } \ No newline at end of file diff --git a/apps/rowboat/app/lib/prebuilt-cards/index.ts b/apps/rowboat/app/lib/prebuilt-cards/index.ts index 16f2305f..1d2e9dd3 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/index.ts +++ b/apps/rowboat/app/lib/prebuilt-cards/index.ts @@ -10,7 +10,7 @@ import tweetWithGeneratedImage from './tweet-with-generated-image.json'; import customerSupport from './customer-support.json'; import githubIssueToSlack from './github-issue-to-slack.json'; import githubPrToSlack from './github-pr-to-slack.json'; -import eisenhowerEmailOrganiser from './eisenhower-email-organiser.json'; +import eisenhowerEmailOrganizer from './eisenhower-email-organizer.json'; // Keep keys consistent with prior file basenames to avoid breaking links. export const prebuiltTemplates = { @@ -23,6 +23,6 @@ export const prebuiltTemplates = { 'Customer Support': customerSupport, 'GitHub Issue to Slack': githubIssueToSlack, 'GitHub PR to Slack': githubPrToSlack, - 'Eisenhower Email Organiser': eisenhowerEmailOrganiser, + 'Eisenhower Email Organizer': eisenhowerEmailOrganizer, }; diff --git a/apps/rowboat/app/lib/prebuilt-cards/tweet-with-generated-image.json b/apps/rowboat/app/lib/prebuilt-cards/tweet-with-generated-image.json index 7cd371c5..b68d2e34 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/tweet-with-generated-image.json +++ b/apps/rowboat/app/lib/prebuilt-cards/tweet-with-generated-image.json @@ -293,7 +293,7 @@ "pipelines": [], "startAgent": "Tweet Assistant", "lastUpdatedAt": "2025-09-11T18:02:35.880Z", - "name": "Viral Tweet Assistant", + "name": "Tweet Assistant", "description": "Research topics and create a tweet including generated images.", "category": "News & Social", "copilotPrompt": "Give me a brief explanation of this assistant." diff --git a/apps/rowboat/app/projects/components/build-assistant-section.tsx b/apps/rowboat/app/projects/components/build-assistant-section.tsx index b6c51b69..9247343d 100644 --- a/apps/rowboat/app/projects/components/build-assistant-section.tsx +++ b/apps/rowboat/app/projects/components/build-assistant-section.tsx @@ -489,7 +489,7 @@ export function BuildAssistantSection() { const workTemplates = templates.filter((t) => (t.category || '').toLowerCase() === 'work productivity'); const devTemplates = templates.filter((t) => (t.category || '').toLowerCase() === 'developer productivity'); const newsTemplates = templates.filter((t) => (t.category || '').toLowerCase() === 'news & social'); - const customerSupportTemplates = templates.filter((t) => (t.category || '').toLowerCase() === 'customer support'); + const customerSupportTemplates = templates.filter((t) => (t.category || '').toLowerCase() === 'support'); const renderGrid = (items: any[]) => (
@@ -593,7 +593,7 @@ export function BuildAssistantSection() {
- Customer Support + Support
{renderGrid(customerSupportTemplates)} From 62c1230cfff30db5e151faa4361fb834ca74196b Mon Sep 17 00:00:00 2001 From: arkml Date: Mon, 15 Sep 2025 17:17:22 +0530 Subject: [PATCH 04/28] remove + more tools option from toolkit in tools view --- .../projects/[projectId]/workflow/entity_list.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 4e402f61..3c704029 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -1532,10 +1532,6 @@ const ComposioCard = ({ case 'remove-toolkit': setShowRemoveToolkitModal(true); break; - case 'more-tools': - setSelectedToolkitSlug(card.slug); - setShowToolsModal(true); - break; } }} disabledKeys={[ @@ -1543,12 +1539,6 @@ const ComposioCard = ({ ...(isProcessingRemove ? ['remove-toolkit'] : []), ]} > - } - > - More tools - {hasToolkitWithAuth && isToolkitConnected ? ( ); -} \ No newline at end of file +} From be4e17b5a5c682866cce8c353259de54c259ef7c Mon Sep 17 00:00:00 2001 From: Akhilesh Sudhakar <55130408+akhisud3195@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:53:35 +0400 Subject: [PATCH 05/28] Community cards and prebuilt cards (#258) * Add community sharing feature and merge with pre-built templates * Add warning before publishing * [Untested] Add delete flow for community cards * Fix bug with sorting by likes count and update design of cards * Fix community assistant parsing errors * Remove all as a type filter * Remove default assistant name for publishing to community * Update DB calls to be standardized paginated --- apps/rowboat/app/actions/project.actions.ts | 22 +- .../assistant-templates/[id]/like/route.ts | 30 ++ .../app/api/assistant-templates/[id]/route.ts | 57 +++ .../assistant-templates/categories/route.ts | 16 + .../app/api/assistant-templates/route.ts | 130 +++++ apps/rowboat/app/api/me/route.ts | 19 + apps/rowboat/app/api/templates/route.ts | 7 - .../app/lib/assistant_templates_seed.ts | 52 ++ apps/rowboat/app/lib/project_templates.ts | 71 +-- .../workflow/components/TopBar.tsx | 332 +++++++++++-- .../[projectId]/workflow/workflow_editor.tsx | 52 ++ .../components/build-assistant-section.tsx | 425 +++++++++------- .../components/common/AssistantCard.tsx | 307 ++++++++++++ .../components/common/AssistantSection.tsx | 238 +++++++++ .../common/UnifiedTemplatesSection.tsx | 468 ++++++++++++++++++ .../projects/create-project.use-case.ts | 23 + .../src/entities/models/assistant-template.ts | 42 ++ .../infrastructure/mongodb/ensure-indexes.ts | 3 + .../mongodb.assistant-templates.repository.ts | 92 ++++ .../mongodb.community-assistants.indexes.ts | 22 + 20 files changed, 2144 insertions(+), 264 deletions(-) create mode 100644 apps/rowboat/app/api/assistant-templates/[id]/like/route.ts create mode 100644 apps/rowboat/app/api/assistant-templates/[id]/route.ts create mode 100644 apps/rowboat/app/api/assistant-templates/categories/route.ts create mode 100644 apps/rowboat/app/api/assistant-templates/route.ts create mode 100644 apps/rowboat/app/api/me/route.ts delete mode 100644 apps/rowboat/app/api/templates/route.ts create mode 100644 apps/rowboat/app/lib/assistant_templates_seed.ts create mode 100644 apps/rowboat/components/common/AssistantCard.tsx create mode 100644 apps/rowboat/components/common/AssistantSection.tsx create mode 100644 apps/rowboat/components/common/UnifiedTemplatesSection.tsx create mode 100644 apps/rowboat/src/entities/models/assistant-template.ts create mode 100644 apps/rowboat/src/infrastructure/repositories/mongodb.assistant-templates.repository.ts create mode 100644 apps/rowboat/src/infrastructure/repositories/mongodb.community-assistants.indexes.ts diff --git a/apps/rowboat/app/actions/project.actions.ts b/apps/rowboat/app/actions/project.actions.ts index 5f042694..54849645 100644 --- a/apps/rowboat/app/actions/project.actions.ts +++ b/apps/rowboat/app/actions/project.actions.ts @@ -2,7 +2,8 @@ import { z } from 'zod'; import { container } from "@/di/container"; import { redirect } from "next/navigation"; -import { templates } from "../lib/project_templates"; +// Fetch library templates from the unified assistant templates repository +import { MongoDBAssistantTemplatesRepository } from "@/src/infrastructure/repositories/mongodb.assistant-templates.repository"; import { authCheck } from "./auth.actions"; import { ApiKey } from "@/src/entities/models/api-key"; import { Project } from "@/src/entities/models/project"; @@ -40,14 +41,17 @@ const updateLiveWorkflowController = container.resolve('revertToLiveWorkflowController'); export async function listTemplates() { - const templatesArray = Object.entries(templates) - .filter(([key]) => key !== 'default') // Exclude the default template - .map(([key, template]) => ({ - id: key, - ...template - })); - - return templatesArray; + const repo = new MongoDBAssistantTemplatesRepository(); + const result = await repo.list({ source: 'library', isPublic: true }, undefined, 100); + // Map to the shape expected by callers (tools at top-level) + return result.items.map((item) => ({ + id: item.id, + name: item.name, + description: item.description, + category: item.category, + tools: (item as any).workflow?.tools || [], + copilotPrompt: item.copilotPrompt, + })); } export async function projectAuthCheck(projectId: string) { diff --git a/apps/rowboat/app/api/assistant-templates/[id]/like/route.ts b/apps/rowboat/app/api/assistant-templates/[id]/like/route.ts new file mode 100644 index 00000000..606f8dbc --- /dev/null +++ b/apps/rowboat/app/api/assistant-templates/[id]/like/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository'; + +const repo = new MongoDBAssistantTemplatesRepository(); + +const ToggleLikeSchema = z.object({ + guestId: z.string().min(1), +}); + +export async function POST(req: NextRequest, context: { params: Promise<{ id: string }> }) { + try { + // Prefer header like existing community route + const guestId = req.headers.get('x-guest-id') || undefined; + const body = !guestId ? await req.json().catch(() => ({})) : {}; + const parsed = ToggleLikeSchema.safeParse({ guestId: guestId || body.guestId }); + if (!parsed.success) { + return NextResponse.json({ error: 'Missing guestId' }, { status: 400 }); + } + + const { id } = await context.params; + const result = await repo.toggleLike(id, parsed.data.guestId); + return NextResponse.json(result); + } catch (error) { + console.error('Error toggling like:', error); + return NextResponse.json({ error: 'Failed to toggle like' }, { status: 500 }); + } +} + + diff --git a/apps/rowboat/app/api/assistant-templates/[id]/route.ts b/apps/rowboat/app/api/assistant-templates/[id]/route.ts new file mode 100644 index 00000000..d0f7217b --- /dev/null +++ b/apps/rowboat/app/api/assistant-templates/[id]/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository'; +import { authCheck } from '@/app/actions/auth.actions'; +import { USE_AUTH } from '@/app/lib/feature_flags'; + +const repo = new MongoDBAssistantTemplatesRepository(); + +export async function GET(_req: NextRequest, context: { params: Promise<{ id: string }> }) { + try { + const { id } = await context.params; + const item = await repo.fetch(id); + if (!item) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + return NextResponse.json(item); + } catch (error) { + console.error('Error fetching assistant template:', error); + return NextResponse.json({ error: 'Failed to fetch assistant template' }, { status: 500 }); + } +} + +export async function DELETE(_req: NextRequest, context: { params: Promise<{ id: string }> }) { + try { + const { id } = await context.params; + const item = await repo.fetch(id); + if (!item) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } + + // Disallow deleting library/prebuilt items + if ((item as any).source === 'library' || item.authorId === 'rowboat-system') { + return NextResponse.json({ error: 'Not allowed' }, { status: 403 }); + } + + let user; + if (USE_AUTH) { + user = await authCheck(); + } else { + user = { id: 'guest_user' } as any; // guest mode acts as a single user + } + + if (item.authorId !== user.id) { + // Do not reveal existence + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } + + const ok = await repo.deleteByIdAndAuthor(id, user.id); + if (!ok) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting assistant template:', error); + return NextResponse.json({ error: 'Failed to delete assistant template' }, { status: 500 }); + } +} + + diff --git a/apps/rowboat/app/api/assistant-templates/categories/route.ts b/apps/rowboat/app/api/assistant-templates/categories/route.ts new file mode 100644 index 00000000..58e5e120 --- /dev/null +++ b/apps/rowboat/app/api/assistant-templates/categories/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository'; + +const repo = new MongoDBAssistantTemplatesRepository(); + +export async function GET(_req: NextRequest) { + try { + const categories = await repo.getCategories(); + return NextResponse.json({ items: categories }); + } catch (error) { + console.error('Error fetching categories:', error); + return NextResponse.json({ error: 'Failed to fetch categories' }, { status: 500 }); + } +} + + diff --git a/apps/rowboat/app/api/assistant-templates/route.ts b/apps/rowboat/app/api/assistant-templates/route.ts new file mode 100644 index 00000000..03389fae --- /dev/null +++ b/apps/rowboat/app/api/assistant-templates/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository'; +import { ensureLibraryTemplatesSeeded } from '@/app/lib/assistant_templates_seed'; +import { authCheck } from '@/app/actions/auth.actions'; +import { auth0 } from '@/app/lib/auth0'; +import { USE_AUTH } from '@/app/lib/feature_flags'; + +const repo = new MongoDBAssistantTemplatesRepository(); + +const ListSchema = z.object({ + category: z.string().optional(), + search: z.string().optional(), + featured: z.boolean().optional(), + source: z.enum(['library','community']).optional(), + cursor: z.string().optional(), + limit: z.number().min(1).max(50).default(20), +}); + +const CreateSchema = z.object({ + name: z.string().min(1).max(100), + description: z.string().min(1).max(500), + category: z.string().min(1), + tags: z.array(z.string()).max(10), + isAnonymous: z.boolean().default(false), + workflow: z.any(), + copilotPrompt: z.string().optional(), + thumbnailUrl: z.string().url().optional(), +}); + +export async function GET(req: NextRequest) { + try { + // Ensure library JSONs are seeded into the unified collection (idempotent) + await ensureLibraryTemplatesSeeded(); + const { searchParams } = new URL(req.url); + const params = ListSchema.parse({ + category: searchParams.get('category') || undefined, + search: searchParams.get('search') || undefined, + featured: searchParams.get('featured') ? searchParams.get('featured') === 'true' : undefined, + source: (searchParams.get('source') as 'library' | 'community') || undefined, + cursor: searchParams.get('cursor') || undefined, + limit: searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : 20, + }); + + // If source specified, query that subset; otherwise return combined from the unified collection + if (params.source === 'library' || params.source === 'community') { + const result = await repo.list({ + category: params.category, + search: params.search, + featured: params.featured, + isPublic: true, + source: params.source, + }, params.cursor, params.limit); + return NextResponse.json(result); + } + + // No source: combine both subsets from the unified collection + const [lib, com] = await Promise.all([ + repo.list({ category: params.category, search: params.search, featured: params.featured, isPublic: true, source: 'library' }, undefined, params.limit), + repo.list({ category: params.category, search: params.search, featured: params.featured, isPublic: true, source: 'community' }, undefined, params.limit), + ]); + return NextResponse.json({ items: [...lib.items, ...com.items], nextCursor: null }); + } catch (error) { + console.error('Error listing assistant templates:', error); + return NextResponse.json({ error: 'Failed to list assistant templates' }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + let user; + if (USE_AUTH) { + user = await authCheck(); + } else { + user = { id: 'guest', email: 'guest@example.com' }; + } + + const body = await req.json(); + const data = CreateSchema.parse(body); + + let authorName = 'Anonymous'; + let authorEmail: string | undefined; + if (USE_AUTH) { + try { + const { user: auth0User } = await auth0.getSession() || {}; + if (auth0User) { + authorName = auth0User.name ?? auth0User.email ?? 'Anonymous'; + authorEmail = auth0User.email; + } + } catch (error) { + console.warn('Could not get Auth0 user info:', error); + } + } + + if (data.isAnonymous) { + authorName = 'Anonymous'; + authorEmail = undefined; + } + + const created = await repo.create({ + name: data.name, + description: data.description, + category: data.category, + authorId: user.id, + authorName, + authorEmail, + isAnonymous: data.isAnonymous, + workflow: data.workflow, + tags: data.tags, + copilotPrompt: data.copilotPrompt, + thumbnailUrl: data.thumbnailUrl, + downloadCount: 0, + likeCount: 0, + featured: false, + isPublic: true, + likes: [], + source: 'community', + }); + + return NextResponse.json(created); + } catch (error) { + console.error('Error creating assistant template:', error); + if (error instanceof z.ZodError) { + return NextResponse.json({ error: 'Invalid request data', details: error.errors }, { status: 400 }); + } + return NextResponse.json({ error: 'Failed to create assistant template' }, { status: 500 }); + } +} + + diff --git a/apps/rowboat/app/api/me/route.ts b/apps/rowboat/app/api/me/route.ts new file mode 100644 index 00000000..b1591439 --- /dev/null +++ b/apps/rowboat/app/api/me/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { authCheck } from '@/app/actions/auth.actions'; +import { USE_AUTH } from '@/app/lib/feature_flags'; + +export async function GET(_req: NextRequest) { + try { + let user; + if (USE_AUTH) { + user = await authCheck(); + } else { + user = { id: 'guest_user' } as any; + } + return NextResponse.json({ id: user.id }); + } catch (error) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } +} + + diff --git a/apps/rowboat/app/api/templates/route.ts b/apps/rowboat/app/api/templates/route.ts deleted file mode 100644 index 3bba07fc..00000000 --- a/apps/rowboat/app/api/templates/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from 'next/server'; -import { templates } from '@/app/lib/project_templates'; - -export async function GET() { - // The templates are now dynamically loaded from JSON files in the templates folder - return NextResponse.json(templates); -} diff --git a/apps/rowboat/app/lib/assistant_templates_seed.ts b/apps/rowboat/app/lib/assistant_templates_seed.ts new file mode 100644 index 00000000..d577f3a4 --- /dev/null +++ b/apps/rowboat/app/lib/assistant_templates_seed.ts @@ -0,0 +1,52 @@ +import { db } from "@/app/lib/mongodb"; +import { prebuiltTemplates } from "@/app/lib/prebuilt-cards"; + +// idempotent seed: creates library (prebuilt) templates in DB if missing +// Uses name+authorName match to avoid duplicates; tags include a stable prebuilt key +export async function ensureLibraryTemplatesSeeded(): Promise { + try { + const collection = db.collection("assistant_templates"); + const now = new Date().toISOString(); + + const entries = Object.entries(prebuiltTemplates); + for (const [prebuiltKey, tpl] of entries) { + // minimal guard; only ingest valid workflow-like objects + if (!(tpl as any)?.agents || !Array.isArray((tpl as any).agents)) continue; + + const name = (tpl as any).name || prebuiltKey; + + // check if already present (by name + authorName Rowboat and special tag) + const existing = await collection.findOne({ name, authorName: "Rowboat", tags: { $in: [ `prebuilt:${prebuiltKey}`, "__library__" ] } }); + if (existing) continue; + + const doc = { + name, + description: (tpl as any).description || "", + category: (tpl as any).category || "Other", + authorId: "rowboat-system", + authorName: "Rowboat", + authorEmail: undefined, + isAnonymous: false, + workflow: tpl as any, + tags: ["__library__", `prebuilt:${prebuiltKey}`].filter(Boolean), + publishedAt: now, + lastUpdatedAt: now, + downloadCount: 0, + likeCount: 0, + featured: false, + isPublic: true, + likes: [] as string[], + copilotPrompt: (tpl as any).copilotPrompt || undefined, + thumbnailUrl: undefined, + source: 'library' as const, + } as const; + + await collection.insertOne(doc as any); + } + } catch (err) { + // best-effort seed; do not throw to avoid breaking requests + console.error("ensureLibraryTemplatesSeeded error:", err); + } +} + + diff --git a/apps/rowboat/app/lib/project_templates.ts b/apps/rowboat/app/lib/project_templates.ts index 3226d1c6..62a89810 100644 --- a/apps/rowboat/app/lib/project_templates.ts +++ b/apps/rowboat/app/lib/project_templates.ts @@ -1,50 +1,33 @@ import { WorkflowTemplate } from "./types/workflow_types"; import { z } from 'zod'; -import { prebuiltTemplates } from './prebuilt-cards'; -const DEFAULT_MODEL = process.env.PROVIDER_DEFAULT_MODEL || "gpt-4.1"; - -// Build templates object using static imports so Vercel bundles them -function buildTemplates(): { [key: string]: z.infer } { - const templates: { [key: string]: z.infer } = {}; - - // Add default template - templates['default'] = { - name: 'Blank Template', - description: 'A blank canvas to build your agents.', - startAgent: "", - agents: [], - prompts: [], - tools: [ - { - name: "Generate Image", - description: "Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.", - isGeminiImage: true, - parameters: { - type: 'object', - properties: { - prompt: { type: 'string', description: 'Text prompt describing the image to generate' }, - modelName: { type: 'string', description: 'Optional Gemini model override' }, - }, - required: ['prompt'], - additionalProperties: true, +// Provide a minimal default template to satisfy legacy code paths that +// still reference `templates.default`. Real templates are DB-backed. +const defaultTemplate: z.infer = { + name: 'Blank Template', + description: 'A blank canvas to build your assistant.', + startAgent: "", + agents: [], + prompts: [], + tools: [ + { + name: "Generate Image", + description: "Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.", + isGeminiImage: true, + parameters: { + type: 'object', + properties: { + prompt: { type: 'string', description: 'Text prompt describing the image to generate' }, + modelName: { type: 'string', description: 'Optional Gemini model override' }, }, + required: ['prompt'], + additionalProperties: true, }, - ], - }; + }, + ], + pipelines: [], +}; - // Merge static prebuilt templates - Object.entries(prebuiltTemplates).forEach(([key, tpl]) => { - // Basic guard to avoid bad entries - if ((tpl as any)?.agents && Array.isArray((tpl as any).agents)) { - templates[key] = tpl as z.infer; - } - }); - - return templates; -} - -export const templates: { [key: string]: z.infer } = buildTemplates(); - -// Note: Prebuilt cards are now loaded from app/lib/prebuilt-cards/ directory -// starting_copilot_prompts has been removed as it was unused +export const templates: Record> = { + default: defaultTemplate, +}; diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx index 312dc7b4..b535829a 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx @@ -1,10 +1,12 @@ -"use client"; + "use client"; import React from "react"; -import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup, Checkbox, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"; +import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup, Checkbox, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, Textarea, Select, SelectItem, Chip, Radio, RadioGroup } from "@heroui/react"; import { Button as CustomButton } from "@/components/ui/button"; import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon, ShareIcon } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { ProgressBar, ProgressStep } from "@/components/ui/progress-bar"; +import { useUser } from '@auth0/nextjs-auth0'; +import { useState, useEffect } from "react"; interface TopBarProps { localProjectName: string; @@ -42,6 +44,20 @@ interface TopBarProps { onShareWorkflow: () => void; shareUrl: string | null; onCopyShareUrl: () => void; + shareMode: 'url' | 'community'; + setShareMode: (mode: 'url' | 'community') => void; + communityData: { + name: string; + description: string; + category: string; + tags: string[]; + isAnonymous: boolean; + copilotPrompt: string; + }; + setCommunityData: (data: any) => void; + onCommunityPublish: () => void; + communityPublishing: boolean; + communityPublishSuccess: boolean; } export function TopBar({ @@ -80,6 +96,13 @@ export function TopBar({ onShareWorkflow, shareUrl, onCopyShareUrl, + shareMode, + setShareMode, + communityData, + setCommunityData, + onCommunityPublish, + communityPublishing, + communityPublishSuccess, }: TopBarProps) { const router = useRouter(); const params = useParams(); @@ -87,11 +110,39 @@ export function TopBar({ // Share modal state const { isOpen: isShareModalOpen, onOpen: onShareModalOpen, onClose: onShareModalClose } = useDisclosure(); + const { isOpen: isConfirmOpen, onOpen: onConfirmOpen, onClose: onConfirmClose } = useDisclosure(); + const [acknowledged, setAcknowledged] = useState(false); + const [copyButtonText, setCopyButtonText] = useState('Copy'); const handleShareClick = () => { onShareWorkflow(); // Call the original share function to generate URL onShareModalOpen(); // Open the modal }; + + const handleCopyUrl = () => { + onCopyShareUrl(); // Call the original copy function + setCopyButtonText('Copied!'); + setTimeout(() => { + setCopyButtonText('Copy'); + }, 2000); // Reset after 2 seconds + }; + + // After successful community publish, briefly show success and then close modal + useEffect(() => { + if (communityPublishSuccess) { + const timer = setTimeout(() => { + onShareModalClose(); + }, 1200); + return () => clearTimeout(timer); + } + }, [communityPublishSuccess, onShareModalClose]); + + const { user } = useUser(); + + const getUserDisplayName = () => { + if (!user) return 'Anonymous'; + return user.name ?? user.email ?? 'Anonymous'; + }; // Progress bar steps with completion logic and current step detection const step1Complete = hasAgentInstructionChanges; @@ -596,46 +647,261 @@ export function TopBar({
{/* Share Modal */} - + - Share Assistant +

Share Assistant

+

Choose how you'd like to share your assistant

-
-

- Share this assistant with others using the URL below: -

- {shareUrl ? ( -
- - +
+ {/* Quick Share Section */} +
+
+
+ +
+
+

Quick Share

+

Share with a direct link

+
- ) : ( -
- - - Generating share URL... - + + {shareUrl ? ( +
+
+ +
+ +
+ ) : ( +
+ + + Generating share URL... + +
+ )} +
+ + {/* Divider */} +
+
+
- )} +
+ or +
+
+ + {/* Community Publishing Section */} +
+
+
+ +
+
+

Publish to Community

+

Make it discoverable by others

+
+
+ +
+ {/* Assistant Name */} +
+ + setCommunityData({ ...communityData, name: e.target.value })} + classNames={{ + input: "text-sm focus:outline-none !focus:ring-0 !focus:ring-offset-0 !ring-0 !ring-offset-0", + inputWrapper: "border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500 focus-within:border-gray-300 dark:focus-within:border-gray-500 !focus-within:ring-0 !focus-within:ring-offset-0 !ring-0 !ring-offset-0" + }} + /> +
+ + {/* Description */} +
+ +