Skip to content
Merged

Test #81

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
c17c1fb
Email - hello world inbound email webhook
sweetmantech Dec 16, 2025
41abe4a
Merge pull request #70 from Recoupable-com/sweetmantech/myc-3777-emai…
sweetmantech Dec 16, 2025
fb8472b
Email - hard-coded response
sweetmantech Dec 16, 2025
e7ca41b
SRP - respondToInboundEmail
sweetmantech Dec 16, 2025
7c87def
remove unused import
sweetmantech Dec 16, 2025
7f4efa1
new from address
sweetmantech Dec 16, 2025
8f4ac57
Merge pull request #71 from Recoupable-com/sweetmantech/myc-3780-emai…
sweetmantech Dec 16, 2025
966606e
Email - lookup account ID in supabase with selectAccountEmails
sweetmantech Dec 16, 2025
d707b6e
KISS - less fallback logic
sweetmantech Dec 16, 2025
95589c7
DRY - from variable.
sweetmantech Dec 16, 2025
e738f9b
JSDoc is inconsistent with implementation.
sweetmantech Dec 16, 2025
369e2c2
Avoid logging full email event payload.
sweetmantech Dec 16, 2025
548835a
Merge pull request #72 from Recoupable-com/sweetmantech/myc-3781-emai…
sweetmantech Dec 16, 2025
93720a6
Email - Recoup Chat Response
sweetmantech Dec 16, 2025
8463ee6
add AI generated response to email response
sweetmantech Dec 16, 2025
325f8f1
update AI email response to be the text
sweetmantech Dec 16, 2025
7474943
KISS - remove dev comments
sweetmantech Dec 16, 2025
6e47000
move system prompt to chat specific lib file.
sweetmantech Dec 16, 2025
7c7c316
move extractImageUrlsFromMessages to lib/messages
sweetmantech Dec 16, 2025
5a56550
remove middleware setupChatRequest function
sweetmantech Dec 16, 2025
283719f
remove unused packages
sweetmantech Dec 16, 2025
d66e9ec
Merge pull request #73 from Recoupable-com/sweetmantech/myc-3778-emai…
sweetmantech Dec 16, 2025
0d24295
Email - pass input text / html to Recoup agent call
sweetmantech Dec 17, 2025
dcc8383
SRP - getEmailContent
sweetmantech Dec 17, 2025
638ec7c
remove dev comments
sweetmantech Dec 17, 2025
987022b
rollback subject changes
sweetmantech Dec 17, 2025
5edb7a3
Merge pull request #74 from Recoupable-com/sweetmantech/myc-3783-emai…
sweetmantech Dec 17, 2025
0c26721
Email - DNS - custom name
sweetmantech Dec 17, 2025
b152816
add name
sweetmantech Dec 17, 2025
a348a50
capitalize first letter
sweetmantech Dec 17, 2025
241e351
SRP - getFromWithName
sweetmantech Dec 17, 2025
c398557
reduce dev comments
sweetmantech Dec 17, 2025
6e2c6bd
case insensitive handling
sweetmantech Dec 17, 2025
8a1086d
Merge pull request #75 from Recoupable-com/sweetmantech/myc-3785-emai…
sweetmantech Dec 17, 2025
3ba551d
simple logging
sweetmantech Dec 17, 2025
2243294
WIP - handleChatCompletion
sweetmantech Dec 17, 2025
78ac7b7
handleChatCompletion
sweetmantech Dec 18, 2025
147cb5c
add logging for roomId
sweetmantech Dec 18, 2025
5560e88
KISS principle - les code is more readable
sweetmantech Dec 18, 2025
b67d6b4
remove dev logging
sweetmantech Dec 18, 2025
663c721
change response to be from assistant
sweetmantech Dec 18, 2025
3324252
remove error handling in completion handler
sweetmantech Dec 18, 2025
c1a992b
SRP - createNewRoom and saveNewMessage
sweetmantech Dec 18, 2025
2dd9382
remove unused lib
sweetmantech Dec 18, 2025
e41af3c
Merge pull request #76 from Recoupable-com/sweetmantech/myc-3779-emai…
sweetmantech Dec 18, 2025
8495b69
add schema for memory_emails table.
sweetmantech Dec 18, 2025
96c22f2
API - Email - new emails saved to memory_emails table
sweetmantech Dec 18, 2025
1b116b4
add dev logging
sweetmantech Dec 18, 2025
bea2476
more logging
sweetmantech Dec 18, 2025
14b20fc
rollback response logic
sweetmantech Dec 18, 2025
6a29a23
rollback logic for sendEmail response changes
sweetmantech Dec 18, 2025
b342309
remove data parsing from response
sweetmantech Dec 18, 2025
e27ad50
Merge pull request #77 from Recoupable-com/sweetmantech/myc-3792-api-…
sweetmantech Dec 18, 2025
27692e9
API - Email - lookup referenced email in content.headers.references t…
sweetmantech Dec 18, 2025
143bf10
https://linear.app/mycowtf/issue/MYC-3793/api-email-lookup-referenced…
sweetmantech Dec 18, 2025
d79db5e
Merge pull request #78 from Recoupable-com/sweetmantech/myc-3793-api-…
sweetmantech Dec 18, 2025
637d639
lib to select records from the memories table.
sweetmantech Dec 18, 2025
d985bf3
pass memories from chat room into agent generate context
sweetmantech Dec 18, 2025
5badb15
SRP - getEmailRoomMessages.
sweetmantech Dec 18, 2025
4f1ec72
logging for messages
sweetmantech Dec 18, 2025
f776484
message logging with ascending filter
sweetmantech Dec 18, 2025
602fa19
remove dev logging
sweetmantech Dec 18, 2025
f982f48
Merge pull request #79 from Recoupable-com/sweetmantech/myc-3794-api-…
sweetmantech Dec 18, 2025
0508a22
API - email - prevent duplicate responses for the same prompt
sweetmantech Dec 18, 2025
22403d8
SRP - validateNewMemory
sweetmantech Dec 20, 2025
37b104f
move insertMemoryEmail call into validateNewMemory
sweetmantech Dec 20, 2025
587c90b
refactor to validateNewEmailMemory
sweetmantech Dec 20, 2025
f4bd7cd
remove dev comments
sweetmantech Dec 20, 2025
9c17026
remove duplicate message inclusion
sweetmantech Dec 20, 2025
e6f7428
Merge pull request #80 from Recoupable-com/sweetmantech/myc-3796-api-…
sweetmantech Dec 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/api/emails/inbound/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { NextRequest } from "next/server";
import { handleInboundEmail } from "@/lib/emails/inbound/handleInboundEmail";

export const POST = async (request: NextRequest) => {
return handleInboundEmail(request);
};
65 changes: 65 additions & 0 deletions lib/agents/generalAgent/getGeneralAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { stepCountIs, ToolLoopAgent } from "ai";
import { DEFAULT_MODEL } from "@/lib/const";
import { RoutingDecision } from "@/lib/chat/types";
import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages";
import { buildSystemPromptWithImages } from "@/lib/chat/buildSystemPromptWithImages";
import { getSystemPrompt } from "@/lib/prompts/getSystemPrompt";
import { setupToolsForRequest } from "@/lib/chat/setupToolsForRequest";
import { ChatRequestBody } from "@/lib/chat/validateChatRequest";
import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails";
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText";
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";

/**
* Gets the general agent for the chat
*
* @param body - The chat request body
* @returns The general agent
*/
export default async function getGeneralAgent(body: ChatRequestBody): Promise<RoutingDecision> {
const { accountId, messages, artistId, model: bodyModel } = body;

const accountEmails = await selectAccountEmails({ accountIds: accountId });
const email = accountEmails[0]?.email || undefined;

// Fetch artist instruction and knowledge base if artistId is provided
let artistInstruction: string | undefined;
let knowledgeBaseText: string | undefined;
if (artistId) {
const artistAccountInfo = await selectAccountInfo(artistId);
artistInstruction = artistAccountInfo?.instruction || undefined;
knowledgeBaseText = await getKnowledgeBaseText(artistAccountInfo?.knowledges);
}

const accountWithDetails = await getAccountWithDetails(accountId);
const baseSystemPrompt = getSystemPrompt({
roomId: body.roomId,
artistId,
accountId,
email,
artistInstruction,
knowledgeBaseText,
accountWithDetails,
});
const imageUrls = extractImageUrlsFromMessages(messages);
const instructions = buildSystemPromptWithImages(baseSystemPrompt, imageUrls);

const tools = await setupToolsForRequest(body);
const model = bodyModel || DEFAULT_MODEL;
const stopWhen = stepCountIs(111);

const agent = new ToolLoopAgent({
model,
instructions,
tools,
stopWhen,
});

return {
agent,
model,
instructions,
stopWhen,
};
}
22 changes: 22 additions & 0 deletions lib/ai/generateText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { generateText as generate } from "ai";
import { DEFAULT_MODEL } from "@/lib/const";

const generateText = async ({
system,
prompt,
model,
}: {
system?: string;
prompt: string;
model?: string;
}) => {
const result = await generate({
system,
model: model || DEFAULT_MODEL,
prompt,
});

return result;
};

export default generateText;
14 changes: 14 additions & 0 deletions lib/chat/buildSystemPromptWithImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Appends image URLs to system prompt for GPT to extract for tool parameters
*
* @param basePrompt - The base system prompt
* @param imageUrls - Array of image URLs to append
* @returns System prompt with image URLs appended (if any)
*/
export function buildSystemPromptWithImages(basePrompt: string, imageUrls: string[]): string {
if (imageUrls.length === 0) {
return basePrompt;
}

return `${basePrompt}\n\n**ATTACHED IMAGE URLS (for edit_image imageUrl parameter):**\n${imageUrls.map((url, i) => `- Image ${i}: ${url}`).join("\n")}`;
}
83 changes: 83 additions & 0 deletions lib/chat/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export const SYSTEM_PROMPT = `You are Recoup, a friendly, sharp, and strategic AI assistant specialized in the music industry. Your purpose is to help music executives, artist teams, and self-starting artists analyze fan data, optimize marketing strategies, and improve artist growth.


# Core Capabilities
1. Artist Management: You can manage artist profiles, social media strategy, and overall marketing direction. Always use the available tools to fetch artist data when requested.
2. Fan Analysis: You excel at analyzing fan demographics, engagement metrics, and behavioral segmentation. Always consider platform-specific metrics and patterns.
3. Marketing Funnels: You identify conversion paths, diagnose bottlenecks, and suggest optimizations for acquisition and engagement.
4. Social Media Strategy: You provide platform-specific recommendations tied to fan behavior and artist brand.
5. Actionable Insights: You generate clear, data-informed, and instantly usable recommendations—not generic marketing advice.


# Specialized Knowledge
• Music Industry Trends: You understand the realities of modern artist development, fan behavior, and campaign strategy.
• Platform Optimization: You are fluent in the nuances of Spotify, TikTok, Instagram, YouTube, and more.
• Fan Segmentation: You group fans based on behavior, geography, platform habits, and cultural signals.

You do not give advice in a vacuum—you analyze everything in context. You understand the artists:
• Genre and brand
• Current career stage (emerging, breakout, legacy)
• Cultural relevance
• Position in the market

What works for an underground rapper will not work for a legacy pop act. Your strategies adapt to the moment the artist is in.


# How You Think

You are proactive. When fan or campaign data reveals a trend, niche, or opportunity, you surface it immediately—even if the user did not ask.

Once you identify an opportunity (e.g., growing skateboarding interest in Asia), you think through all viable monetization paths:
1. Content series targeting that niche
2. Influencer partnerships
3. Brand collaborations (tailored by artist tier)
4. Artist collaborations
5. Tour or live event strategies

Then you figure out how to execute:
• If it is a brand play, find regional brands likely to partner based on artist fit and engagement
• If it is an influencer angle, identify relevant creators by niche and geography
• If it is a touring strategy, suggest cities, venues, activations, and content ideas
• Bundle insights into a multi-pronged campaign blueprint

You may suggest one or multiple directions—depending on user interest and feasibility.


# How You Communicate

You are:
• Brief by default, expanding when needed
• Conversational and collaborative—switch between telling and asking
• Warm but strategic—no fluff, just clarity and sharp thinking
• Always focused on next steps without overwhelming the user
• User-friendly—avoid technical jargon like "bytes", "root directory", "storage keys", etc. Speak naturally about creating, updating, or managing files

# Markdown Formatting

Keep it simple:
• Clarity first: short paragraphs; one idea per paragraph.
• Gentle structure: use H2 for sections when helpful; avoid deep nesting.
• Inline labels over bullets: for facets of one idea, use bold labels with a colon (e.g., Concept, Hook, CTA, Why) inline rather than bullets.
• Bullets only when necessary: use bullets only for multiple parallel items; otherwise keep prose.
• Subtle emphasis: bold for key terms; italics for nuance; keep headers plain.
• Minimal extras: at most one callout or a simple table if it meaningfully improves scanning; avoid decorative elements.


🧷 Behavior Rules
• Be proactive when insights emerge from fan or artist data
• Always factor in the artists cultural relevance and career stage
• Avoid generic advice—ground everything in actual data and user context
• Suggest next steps clearly and strategically
• Use tools or data to expand on viable monetization paths
• Adjust depth and complexity based on the artists level (e.g., indie vs. major)
• Acknowledge limitations and adapt creatively with what you can access
• Do NOT ask for permission. Continue until you've accomplished the task


You impress by being useful. Every conversation should feel like something the user could not have come up with on their own.`;
54 changes: 54 additions & 0 deletions lib/chat/createNewRoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { insertRoom } from "@/lib/supabase/rooms/insertRoom";
import { generateChatTitle } from "@/lib/chat/generateChatTitle";
import { sendNewConversationNotification } from "@/lib/telegram/sendNewConversationNotification";
import { UIMessage } from "ai";
import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails";

interface CreateNewRoomParams {
accountId: string;
roomId: string;
artistId?: string;
lastMessage: UIMessage;
}

/**
* Creates a new room and sends a notification for a new conversation.
*
* @param params - The parameters for creating a new room
* @param params.accountId - The account ID
* @param params.roomId - The room ID
* @param params.artistId - Optional artist ID
* @param params.lastMessage - The last message from the conversation
* @returns void
*/
export async function createNewRoom({
accountId,
roomId,
artistId,
lastMessage,
}: CreateNewRoomParams): Promise<void> {
const latestMessageText = lastMessage.parts.find(part => part.type === "text")?.text || "";
const conversationName = await generateChatTitle(latestMessageText);

let email = "";
const accountEmails = await selectAccountEmails({ accountIds: accountId });
if (accountEmails.length > 0 && accountEmails[0].email) {
email = accountEmails[0].email;
}

await Promise.all([
insertRoom({
account_id: accountId,
topic: conversationName,
artist_id: artistId || undefined,
id: roomId,
}),
sendNewConversationNotification({
accountId,
email,
conversationId: roomId,
topic: conversationName,
firstMessage: latestMessageText,
}),
]);
}
20 changes: 20 additions & 0 deletions lib/chat/filterExcludedTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ToolSet } from "ai";

/**
* Filters out excluded tools from a tools object
*
* @param tools - The tools object to filter
* @param excludeTools - Array of tool names to exclude
* @returns Filtered tools object with excluded tools removed
*/
export function filterExcludedTools(tools: ToolSet, excludeTools?: string[]): ToolSet {
if (!excludeTools || excludeTools.length === 0) {
return tools;
}

const filteredTools = Object.fromEntries(
Object.entries(tools).filter(([toolName]) => !excludeTools.includes(toolName)),
);

return filteredTools as ToolSet;
}
21 changes: 21 additions & 0 deletions lib/chat/generateChatTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import generateText from "@/lib/ai/generateText";
import { LIGHTWEIGHT_MODEL } from "@/lib/const";

/**
* Generates a brief, formal title (max 20 characters) based on the given question context.
* Highlights segment names if present in the question.
*
* @param question - The question or context to generate a title for
* @returns A promise that resolves to the generated title string
*/
export async function generateChatTitle(question: string): Promise<string> {
const response = await generateText({
prompt: `Provide a brief title (more formal, no more than 20 characters!!!) that reflects the key elements of the given context. Do not wrap the title in quotes.
If the question is related to a segment or contains a segment name, highlight the segment name.
Context: ${question}`,
model: LIGHTWEIGHT_MODEL,
});

// In case model accidentally generates quotes again, remove them here
return response.text.replace(/^["']|["']$/g, "");
}
28 changes: 28 additions & 0 deletions lib/chat/setupToolsForRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ToolSet } from "ai";
import { filterExcludedTools } from "./filterExcludedTools";
import { ChatRequestBody } from "./validateChatRequest";
import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

/**
* Sets up and filters tools for a chat request.
* This is a simplified version that returns an empty tool set.
* In a full implementation, this would load MCP tools, Google Sheets tools, etc.
*
* @param body - The chat request body
* @returns Filtered tool set ready for use
*/
export async function setupToolsForRequest(body: ChatRequestBody): Promise<ToolSet> {
const { excludeTools } = body;

const mcpClient = await createMCPClient({
transport: new StreamableHTTPClientTransport(new URL("/mcp", "https://recoup-api.vercel.app")),
});

const mcpClientTools = (await mcpClient.tools()) as ToolSet;

const allTools: ToolSet = { ...mcpClientTools };

const tools = filterExcludedTools(allTools, excludeTools);
return tools;
}
9 changes: 9 additions & 0 deletions lib/chat/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { VercelToolCollection } from "@composio/vercel";
import { type ToolSet, type StopCondition, type ToolLoopAgent } from "ai";

export interface RoutingDecision {
model: string;
instructions: string;
agent: ToolLoopAgent<never, VercelToolCollection, never>;
stopWhen?: StopCondition<NoInfer<ToolSet>> | StopCondition<NoInfer<ToolSet>>[] | undefined;
}
37 changes: 37 additions & 0 deletions lib/chat/validateChatRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from "zod";

export const chatRequestSchema = z
.object({
// Chat content
prompt: z.string().optional(),
messages: z.array(z.any()).default([]),
// Core routing / context fields
roomId: z.string().optional(),
accountId: z.string().optional(),
artistId: z.string().optional(),
model: z.string().optional(),
excludeTools: z.array(z.string()).optional(),
})
.superRefine((data, ctx) => {
const hasMessages = Array.isArray(data.messages) && data.messages.length > 0;
const hasPrompt = typeof data.prompt === "string" && data.prompt.trim().length > 0;

if ((hasMessages && hasPrompt) || (!hasMessages && !hasPrompt)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Exactly one of messages or prompt must be provided",
path: ["messages"],
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Exactly one of messages or prompt must be provided",
path: ["prompt"],
});
}
});

type BaseChatRequestBody = z.infer<typeof chatRequestSchema>;

export type ChatRequestBody = BaseChatRequestBody & {
accountId: string;
};
2 changes: 0 additions & 2 deletions lib/coinbase/getAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export async function getAccount(accountId: string): Promise<EvmServerAccount> {
const account = await cdp.evm.getAccount({ name: accountId });
return account;
} catch {
// If account doesn't exist, create it
console.log(`[getAccount] Account ${accountId} not found, creating new account`);
try {
const newAccount = await cdp.evm.createAccount({ name: accountId });
return newAccount;
Expand Down
1 change: 1 addition & 0 deletions lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Addr
export const PAYMASTER_URL = `https://api.developer.coinbase.com/rpc/v1/base/${process.env.PAYMASTER_KEY}`;
export const IMAGE_GENERATE_PRICE = "0.15";
export const DEFAULT_MODEL = "openai/gpt-5-mini";
export const LIGHTWEIGHT_MODEL = "openai/gpt-4o-mini";
export const PRIVY_PROJECT_SECRET = process.env.PRIVY_PROJECT_SECRET;
Loading