diff --git a/.gitignore b/.gitignore index 01b0a0703..350f8adcd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ node_modules .pnp .pnp.js +.docusaurus + .env.development # Local env files diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 1215a7f8d..6965e5346 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -3,8 +3,6 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import { useHistory } from "@docusaurus/router" - - export default function Home(): ReactNode { const { siteConfig } = useDocusaurusContext(); const history = useHistory(); @@ -14,7 +12,6 @@ export default function Home(): ReactNode { history.push("/prototype/docs/Getting Started/getting-started") }, []) - useHistory return ( 0 && + Array.isArray(wishlist.summaryOffers) && wishlist.summaryOffers.length > 0; + + if (hasArraySummaries) { + skippedCount++; + continue; + } + + console.log(`Processing wishlist ${wishlist.id}...`); + console.log(` Raw content: ${wishlist.content.substring(0, 200)}${wishlist.content.length > 200 ? '...' : ''}`); + + // Use the summary service to generate arrays from the raw content + const summary = await summaryService.summarizeWishlistContent(wishlist.content); + + console.log(` Generated Wants: ${JSON.stringify(summary.summaryWants)}`); + console.log(` Generated Offers: ${JSON.stringify(summary.summaryOffers)}`); + + // Save the arrays to the database + wishlist.summaryWants = summary.summaryWants; + wishlist.summaryOffers = summary.summaryOffers; + await wishlistRepository.save(wishlist); + + console.log(` Saved successfully\n`); + processedCount++; + + } catch (error) { + console.error(` Error processing wishlist ${wishlist.id}:`, error); + errorCount++; + } + } + + console.log("Migration completed:"); + console.log(` Processed: ${processedCount}`); + console.log(` Skipped (already has arrays): ${skippedCount}`); + console.log(` Errors: ${errorCount}\n`); + + } catch (error) { + console.error("Migration failed:", error); + process.exit(1); + } finally { + await AppDataSource.destroy(); + console.log("Database connection closed"); + } +} + +// Run migration +migrateSummaries() + .then(() => { + console.log("Migration script completed"); + process.exit(0); + }) + .catch((error) => { + console.error("Migration script failed:", error); + process.exit(1); + }); diff --git a/platforms/dreamsync-api/src/database/entities/Wishlist.ts b/platforms/dreamsync-api/src/database/entities/Wishlist.ts index f5192d134..d6944ef0a 100644 --- a/platforms/dreamsync-api/src/database/entities/Wishlist.ts +++ b/platforms/dreamsync-api/src/database/entities/Wishlist.ts @@ -22,11 +22,11 @@ export class Wishlist { @Column({ type: "text" }) content: string; // Markdown content - @Column({ type: "text", nullable: true }) - summaryWants: string | null; + @Column({ type: "jsonb", nullable: true }) + summaryWants: string[] | null; - @Column({ type: "text", nullable: true }) - summaryOffers: string | null; + @Column({ type: "jsonb", nullable: true }) + summaryOffers: string[] | null; @Column({ type: "boolean", default: true }) isActive: boolean; diff --git a/platforms/dreamsync-api/src/database/migrations/1768904445609-migration.ts b/platforms/dreamsync-api/src/database/migrations/1768904445609-migration.ts new file mode 100644 index 000000000..5bcc39e4a --- /dev/null +++ b/platforms/dreamsync-api/src/database/migrations/1768904445609-migration.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migration1768904445609 implements MigrationInterface { + name = 'Migration1768904445609' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "wishlists" DROP COLUMN "summaryWants"`); + await queryRunner.query(`ALTER TABLE "wishlists" ADD "summaryWants" jsonb`); + await queryRunner.query(`ALTER TABLE "wishlists" DROP COLUMN "summaryOffers"`); + await queryRunner.query(`ALTER TABLE "wishlists" ADD "summaryOffers" jsonb`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "wishlists" DROP COLUMN "summaryOffers"`); + await queryRunner.query(`ALTER TABLE "wishlists" ADD "summaryOffers" text`); + await queryRunner.query(`ALTER TABLE "wishlists" DROP COLUMN "summaryWants"`); + await queryRunner.query(`ALTER TABLE "wishlists" ADD "summaryWants" text`); + } + +} diff --git a/platforms/dreamsync-api/src/index.ts b/platforms/dreamsync-api/src/index.ts index c9a225f72..10a57ca0d 100644 --- a/platforms/dreamsync-api/src/index.ts +++ b/platforms/dreamsync-api/src/index.ts @@ -32,23 +32,24 @@ AppDataSource.initialize() const exists = await platformService.checkPlatformEVaultExists(); if (!exists) { - console.log("🔧 Creating platform eVault for DreamSync..."); + console.log("Creating platform eVault for DreamSync..."); const result = await platformService.createPlatformEVault(); - console.log(`✅ Platform eVault created successfully: ${result.w3id}`); + console.log(`Platform eVault created successfully: ${result.w3id}`); } else { - console.log("✅ Platform eVault already exists for DreamSync"); + console.log("Platform eVault already exists for DreamSync"); } } catch (error) { - console.error("❌ Failed to initialize platform eVault:", error); + console.error("Failed to initialize platform eVault:", error); // Don't exit the process, just log the error } - // Backfill wishlist summaries for existing records + // Summarize all wishlists on platform start try { const wishlistSummaryService = WishlistSummaryService.getInstance(); - await wishlistSummaryService.backfillMissingSummaries(); + await wishlistSummaryService.summarizeAllWishlists(); } catch (error) { - console.error("❌ Failed to backfill wishlist summaries:", error); + console.error("Failed to summarize wishlists:", error); + // Don't exit the process, just log the error } // Start AI matching job (disabled automatic startup) diff --git a/platforms/dreamsync-api/src/services/AIMatchingService.ts b/platforms/dreamsync-api/src/services/AIMatchingService.ts index 56c85a5e7..b653c9e34 100644 --- a/platforms/dreamsync-api/src/services/AIMatchingService.ts +++ b/platforms/dreamsync-api/src/services/AIMatchingService.ts @@ -38,26 +38,38 @@ export class AIMatchingService { return withOperationContext('AIMatchingService', operationId, async () => { const wishlists = await this.getWishlistsForMatching(); - console.log(`📋 Found ${wishlists.length} wishlists to analyze`); + console.log(`📋 Found ${wishlists.length} wishlists to analyze (after filtering blank wishlists)`); + + if (wishlists.length === 0) { + console.log("⚠️ No valid wishlists to match, skipping matching process"); + return; + } + await this.ensureWishlistSummaries(wishlists); // Get existing groups for context const existingGroups = await this.getExistingGroups(); console.log(`🏠 Found ${existingGroups.length} existing groups to consider`); - // Convert to shared service format - const wishlistData: WishlistData[] = wishlists.map(wishlist => ({ - id: wishlist.id, - content: wishlist.content, - summaryWants: wishlist.summaryWants || "", - summaryOffers: wishlist.summaryOffers || "", - userId: wishlist.userId, - user: { - id: wishlist.user.id, - name: wishlist.user.name || wishlist.user.ename, - ename: wishlist.user.ename - } - })); + // Convert to shared service format, filtering out wishlists without summaries + const wishlistData: WishlistData[] = wishlists + .filter(wishlist => { + // Only include wishlists with valid summary arrays + return wishlist.summaryWants && wishlist.summaryWants.length > 0 && + wishlist.summaryOffers && wishlist.summaryOffers.length > 0; + }) + .map(wishlist => ({ + id: wishlist.id, + content: wishlist.content, + summaryWants: wishlist.summaryWants || [], + summaryOffers: wishlist.summaryOffers || [], + userId: wishlist.userId, + user: { + id: wishlist.user.id, + name: wishlist.user.name || wishlist.user.ename, + ename: wishlist.user.ename + } + })); // Use matching service for parallel processing const matchResults = await this.matchingService.findMatches(wishlistData, existingGroups); @@ -224,9 +236,19 @@ export class AIMatchingService { private async ensureWishlistSummaries(wishlists: Wishlist[]): Promise { for (const wishlist of wishlists) { - if (!wishlist.summaryWants || !wishlist.summaryOffers) { + // Ensure summaries exist and are arrays + if (!wishlist.summaryWants || wishlist.summaryWants.length === 0 || + !wishlist.summaryOffers || wishlist.summaryOffers.length === 0) { try { await this.wishlistSummaryService.ensureSummaries(wishlist); + // Reload wishlist to get updated summaries + const updated = await this.wishlistRepository.findOne({ + where: { id: wishlist.id }, + relations: ["user"] + }); + if (updated) { + Object.assign(wishlist, updated); + } } catch (error) { console.error(`Failed to ensure summary for wishlist ${wishlist.id}`, error); } @@ -383,10 +405,12 @@ Content: ${wishlistA.content} Title: ${wishlistB.title} Content: ${wishlistB.content} -IMPORTANT: Only return a JSON response if there's a meaningful connection (confidence > 0.7). If there's no meaningful connection, return confidence: 0 and matchType: "private" (this will be filtered out). +IMPORTANT: Only return a JSON response if there's a meaningful connection (confidence > 0.85). If there's no meaningful connection, return confidence: 0 and matchType: "private" (this will be filtered out). + +CRITICAL: If either wishlist is blank, templated with minimal content, or contains insufficient information, return confidence: 0. A blank/templated wishlist has the template structure (## What I Want / ## What I Can Do) but with very few items (2 or fewer meaningful items) or very short/placeholder content. Do NOT generate matches based on generic or placeholder content. Return JSON with: -1. "confidence": number between 0-1 indicating match strength (0 if no meaningful connection) +1. "confidence": number between 0-1 indicating match strength (0 if no meaningful connection or if wishlists are too sparse) 2. "matchType": "private" or "group" (use "private" if no connection) 3. "reason": brief explanation of why they match (or why no match) 4. "matchedWants": array of what User A wants that User B can offer @@ -399,8 +423,9 @@ Consider: - Complementary needs and offerings - Potential for meaningful collaboration - Whether this could be a private connection or group activity +- Whether the wishlists contain sufficient meaningful content (not just template placeholders) -Only suggest matches with confidence > 0.7 for meaningful connections. +Only suggest matches with confidence > 0.85 for meaningful connections based on substantial content. `.trim(); } diff --git a/platforms/dreamsync-api/src/services/MatchingService.ts b/platforms/dreamsync-api/src/services/MatchingService.ts index 54154a715..487018883 100644 --- a/platforms/dreamsync-api/src/services/MatchingService.ts +++ b/platforms/dreamsync-api/src/services/MatchingService.ts @@ -15,8 +15,8 @@ export interface MatchResult { export interface WishlistData { id: string; content: string; - summaryWants: string; - summaryOffers: string; + summaryWants: string[]; + summaryOffers: string[]; userId: string; user: { id: string; @@ -48,19 +48,34 @@ export class MatchingService { * Analyze all wishlists at once and find matches in a single AI request */ async findMatches(wishlists: WishlistData[], existingGroups?: GroupData[]): Promise { - console.log(`🤖 Starting AI matching process for ${wishlists.length} wishlists...`); - console.log(`📊 Analyzing all wishlists in a single AI request (much more efficient!)`); + console.log(`Starting AI matching process for ${wishlists.length} wishlists...`); + console.log(`Analyzing all wishlists in a single AI request (much more efficient!)`); + + // Filter out wishlists without valid summaries before processing + const validWishlists = wishlists.filter((wishlist) => { + return wishlist.summaryWants && wishlist.summaryWants.length > 0 && + wishlist.summaryOffers && wishlist.summaryOffers.length > 0; + }); + + if (validWishlists.length === 0) { + console.log("No wishlists with valid summaries to match, returning empty array"); + return []; + } + + if (validWishlists.length < wishlists.length) { + console.log(`Filtered out ${wishlists.length - validWishlists.length} wishlists without valid summaries`); + } if (existingGroups && existingGroups.length > 0) { - console.log(`🏠 Found ${existingGroups.length} existing groups to consider`); + console.log(`Found ${existingGroups.length} existing groups to consider`); } try { - const matchResults = await this.analyzeAllMatches(wishlists, existingGroups); - console.log(`🎉 AI matching process completed! Found ${matchResults.length} matches`); + const matchResults = await this.analyzeAllMatches(validWishlists, existingGroups); + console.log(`AI matching process completed! Found ${matchResults.length} matches`); return matchResults; } catch (error) { - console.error("❌ Error in AI matching process:", error); + console.error("Error in AI matching process:", error); return []; } } @@ -69,8 +84,9 @@ export class MatchingService { const delimiter = "<|>"; const wishlistHeader = `userId${delimiter}userEname${delimiter}userName${delimiter}wants${delimiter}offers`; const wishlistRows = wishlists.map((wishlist) => { - const wants = wishlist.summaryWants || wishlist.content; - const offers = wishlist.summaryOffers || wishlist.content; + // Join array items with semicolons for CSV format + const wants = (wishlist.summaryWants || []).join('; '); + const offers = (wishlist.summaryOffers || []).join('; '); return [ this.sanitizeField(wishlist.userId), this.sanitizeField(wishlist.user.ename), @@ -105,21 +121,29 @@ Use the exact groupId from the table above in the format: "JOIN_EXISTING_GROUP:< } return ` -You are an AI matching assistant. Analyze ALL the wishlists below and find meaningful connections between users. +You are an AI matching assistant. Your task is to find meaningful connections between users based on their wishlists. The wishlists are provided as delimiter-separated rows (delimiter: "${delimiter}"). Columns: userId${delimiter}userEname${delimiter}userName${delimiter}wants${delimiter}offers +The "wants" and "offers" columns contain semicolon-separated arrays of short phrases extracted from each user's wishlist. + ${wishlistHeader} ${wishlistRows} Use ONLY the rows above (not full prose) to infer matches. Return userIds EXACTLY as provided in the table (no new IDs, no missing IDs).${existingGroupsText} -TASK: Find ALL meaningful matches between these users based on their wishlists. +CRITICAL INSTRUCTIONS: +- You MUST analyze the wishlists above and find matches +- Look for complementary needs: when User A wants something that User B offers, or vice versa +- Look for shared interests: when multiple users want or offer similar things +- Look for skill exchanges: when one can teach what another wants to learn +- You should actively search for connections - do NOT return an empty array unless there are truly ZERO possible connections +- All wishlists provided have valid content - analyze them thoroughly IMPORTANT RULES: -1. Only suggest matches with confidence > 0.7 +1. Only suggest matches with confidence > 0.85 2. Each user can be matched with multiple other users 3. Return ALL matches found, not just the best ones 4. Consider both "What I Want" and "What I Can Do" sections @@ -178,39 +202,72 @@ Be thorough and find ALL potential matches! const prompt = this.buildAllMatchesPrompt(wishlists, existingGroups); console.log("\n" + "=".repeat(100)); - console.log("🤖 AI REQUEST DEBUG - FULL PROMPT SENT TO AI:"); + console.log("AI REQUEST DEBUG - FULL PROMPT SENT TO AI:"); console.log("=".repeat(100)); console.log(prompt); console.log("=".repeat(100)); - console.log(`📊 Prompt length: ${prompt.length} characters`); - console.log(`📊 Number of wishlists: ${wishlists.length}`); + console.log(`Prompt length: ${prompt.length} characters`); + console.log(`Number of wishlists: ${wishlists.length}`); console.log("=".repeat(100) + "\n"); - const response = await this.openai.chat.completions.create({ - model: "gpt-4", - messages: [ - { - role: "system", - content: "You are an AI matching assistant. Your goal is to find meaningful connections between people based on their wishlists. Analyze all wishlists and return ALL matches found as a JSON array." - }, - { - role: "user", - content: prompt, - }, - ], - temperature: 0.7, - max_tokens: 4000, // Increased token limit for multiple matches - }); + let response; + try { + response = await this.openai.chat.completions.create({ + model: "gpt-4", + messages: [ + { + role: "system", + content: "You are an AI matching assistant. Your goal is to find meaningful connections between people based on their wishlists. You MUST actively search for matches - look for complementary needs, shared interests, and skill exchanges. Return ALL matches found as a JSON array. Only return an empty array if there are genuinely zero possible connections between any users." + }, + { + role: "user", + content: prompt, + }, + ], + temperature: 0.7, + max_tokens: 4000, // Increased token limit for multiple matches + }); + } catch (error: any) { + console.error("OpenAI API Error:"); + console.error(" Error type:", error?.constructor?.name); + console.error(" Error message:", error?.message); + console.error(" Error code:", error?.code); + console.error(" Error status:", error?.status); + + // Check for rate limit errors + if (error?.status === 429 || error?.code === 'rate_limit_exceeded' || error?.message?.includes('rate limit')) { + console.error("RATE LIMIT DETECTED!"); + console.error(" Rate limit error details:", JSON.stringify(error, null, 2)); + throw new Error("OpenAI rate limit exceeded. Please try again later."); + } + + // Check for other API errors + if (error?.status) { + console.error(`OpenAI API returned status ${error.status}`); + console.error(" Full error:", JSON.stringify(error, null, 2)); + } + + throw error; + } const content = response.choices[0]?.message?.content; + // Check for suspiciously short responses (might indicate rate limiting or errors) + if (content && content.length < 10) { + console.warn("WARNING: Received very short response from AI (might indicate rate limiting or error)"); + console.warn(` Response: "${content}"`); + console.warn(` Response length: ${content.length} characters`); + } + console.log("\n" + "=".repeat(100)); - console.log("🤖 AI RESPONSE DEBUG - FULL RESPONSE FROM AI:"); + console.log("AI RESPONSE DEBUG - FULL RESPONSE FROM AI:"); console.log("=".repeat(100)); console.log(content); console.log("=".repeat(100)); - console.log(`📊 Response length: ${content?.length || 0} characters`); - console.log(`📊 Usage: ${JSON.stringify(response.usage, null, 2)}`); + console.log(`Response length: ${content?.length || 0} characters`); + console.log(`Usage: ${JSON.stringify(response.usage, null, 2)}`); + console.log(`Model: ${response.model || 'N/A'}`); + console.log(`Finish reason: ${response.choices[0]?.finish_reason || 'N/A'}`); console.log("=".repeat(100) + "\n"); if (!content) { @@ -221,25 +278,25 @@ Be thorough and find ALL potential matches! // Try to extract JSON array from the response const jsonMatch = content.match(/\[[\s\S]*\]/); if (!jsonMatch) { - console.log("❌ DEBUG: No JSON array pattern found in response"); - console.log("❌ DEBUG: Looking for pattern: /\\[[\\s\\S]*\\]/"); + console.log("DEBUG: No JSON array pattern found in response"); + console.log("DEBUG: Looking for pattern: /\\[[\\s\\S]*\\]/"); throw new Error("No JSON array found in response"); } console.log("\n" + "=".repeat(100)); - console.log("🔍 JSON EXTRACTION DEBUG:"); + console.log("JSON EXTRACTION DEBUG:"); console.log("=".repeat(100)); - console.log("📝 Extracted JSON string:"); + console.log("Extracted JSON string:"); console.log(jsonMatch[0]); console.log("=".repeat(100) + "\n"); const matches = JSON.parse(jsonMatch[0]); console.log("\n" + "=".repeat(100)); - console.log("🔍 PARSED MATCHES DEBUG:"); + console.log("PARSED MATCHES DEBUG:"); console.log("=".repeat(100)); - console.log(`📊 Total matches from AI: ${matches.length}`); - console.log("📝 Raw matches array:"); + console.log(`Total matches from AI: ${matches.length}`); + console.log("Raw matches array:"); console.log(JSON.stringify(matches, null, 2)); console.log("=".repeat(100) + "\n"); @@ -251,7 +308,7 @@ Be thorough and find ALL potential matches! const validMatches: MatchResult[] = []; for (let i = 0; i < matches.length; i++) { const match = matches[i]; - console.log(`🔍 Validating match ${i + 1}:`, JSON.stringify(match, null, 2)); + console.log(`Validating match ${i + 1}:`, JSON.stringify(match, null, 2)); // Check if this is a JOIN_EXISTING_GROUP match (can have 1 user) const isJoinExistingGroup = match.suggestedActivities?.some((activity: any) => @@ -261,13 +318,13 @@ Be thorough and find ALL potential matches! const minUsers = isJoinExistingGroup ? 1 : 2; if (typeof match.confidence === 'number' && - match.confidence > 0.7 && + match.confidence > 0.85 && ['private', 'group'].includes(match.matchType) && Array.isArray(match.userIds) && match.userIds.length >= minUsers && match.activityCategory) { - console.log(`✅ Match ${i + 1} is VALID`); + console.log(`Match ${i + 1} is VALID`); validMatches.push({ confidence: match.confidence, matchType: match.matchType, @@ -280,7 +337,7 @@ Be thorough and find ALL potential matches! activityCategory: match.activityCategory }); } else { - console.log(`❌ Match ${i + 1} is INVALID:`); + console.log(`Match ${i + 1} is INVALID:`); console.log(` - confidence: ${match.confidence} (type: ${typeof match.confidence})`); console.log(` - matchType: ${match.matchType} (valid: ${['private', 'group'].includes(match.matchType)})`); console.log(` - userIds: ${JSON.stringify(match.userIds)} (isArray: ${Array.isArray(match.userIds)}, length: ${match.userIds?.length}, min required: ${minUsers})`); @@ -289,7 +346,7 @@ Be thorough and find ALL potential matches! } } - console.log(`✅ AI found ${validMatches.length} valid matches from ${matches.length} total suggestions`); + console.log(`AI found ${validMatches.length} valid matches from ${matches.length} total suggestions`); return validMatches; } catch (error) { console.error("Failed to parse OpenAI response:", content); diff --git a/platforms/dreamsync-api/src/services/SharedMatchingService.ts b/platforms/dreamsync-api/src/services/SharedMatchingService.ts index aa42134d0..40f894926 100644 --- a/platforms/dreamsync-api/src/services/SharedMatchingService.ts +++ b/platforms/dreamsync-api/src/services/SharedMatchingService.ts @@ -13,8 +13,8 @@ export interface MatchResult { export interface WishlistData { id: string; content: string; - summaryWants: string; - summaryOffers: string; + summaryWants: string[]; + summaryOffers: string[]; userId: string; user: { id: string; @@ -63,7 +63,7 @@ export class MatchingService { const batchPromises = batch.map(async ({ wishlistA, wishlistB }) => { try { const matchResult = await this.analyzeMatch(wishlistA, wishlistB); - if (matchResult.confidence > 0.7) { + if (matchResult.confidence > 0.85) { return matchResult; } return null; @@ -90,19 +90,28 @@ export class MatchingService { } private buildAnalysisPrompt(wishlistA: WishlistData, wishlistB: WishlistData): string { + const wantsA = (wishlistA.summaryWants || []).join(', '); + const offersA = (wishlistA.summaryOffers || []).join(', '); + const wantsB = (wishlistB.summaryWants || []).join(', '); + const offersB = (wishlistB.summaryOffers || []).join(', '); + return ` Analyze these two wishlists to determine if there's a meaningful connection: Wishlist A (User: ${wishlistA.user.name}): - ${wishlistA.content} + Wants: ${wantsA || 'None specified'} + Offers: ${offersA || 'None specified'} Wishlist B (User: ${wishlistB.user.name}): - ${wishlistB.content} + Wants: ${wantsB || 'None specified'} + Offers: ${offersB || 'None specified'} + + IMPORTANT: Only return a JSON response if there's a meaningful connection (confidence > 0.85). If there's no meaningful connection, return confidence: 0 and matchType: "private" (this will be filtered out). - IMPORTANT: Only return a JSON response if there's a meaningful connection (confidence > 0.7). If there's no meaningful connection, return confidence: 0 and matchType: "private" (this will be filtered out). + CRITICAL: If either wishlist is blank, templated with minimal content, or contains insufficient information, return confidence: 0. A blank/templated wishlist has the template structure (## What I Want / ## What I Can Do) but with very few items (2 or fewer meaningful items) or very short/placeholder content. Do NOT generate matches based on generic or placeholder content. Return JSON with: - 1. "confidence": number between 0-1 indicating match strength (0 if no meaningful connection) + 1. "confidence": number between 0-1 indicating match strength (0 if no meaningful connection or if wishlists are too sparse) 2. "matchType": "private" or "group" (use "private" if no connection) 3. "reason": brief explanation of why they match (or why no match) 4. "matchedWants": array of what User A wants that User B can offer @@ -115,8 +124,9 @@ export class MatchingService { - Complementary needs and offerings - Potential for meaningful collaboration - Whether this could be a private connection or group activity + - Whether the wishlists contain sufficient meaningful content (not just template placeholders) - Only suggest matches with confidence > 0.7 for meaningful connections. + Only suggest matches with confidence > 0.85 for meaningful connections based on substantial content. `.trim(); } diff --git a/platforms/dreamsync-api/src/services/WishlistSummaryService.ts b/platforms/dreamsync-api/src/services/WishlistSummaryService.ts index 2e2e69fc8..e8dfe2fab 100644 --- a/platforms/dreamsync-api/src/services/WishlistSummaryService.ts +++ b/platforms/dreamsync-api/src/services/WishlistSummaryService.ts @@ -4,8 +4,8 @@ import { AppDataSource } from "../database/data-source"; import { Wishlist } from "../database/entities/Wishlist"; type WishlistSummary = { - summaryWants: string; - summaryOffers: string; + summaryWants: string[]; + summaryOffers: string[]; }; const DELIMITER = "<|>"; @@ -32,13 +32,22 @@ export class WishlistSummaryService { async summarizeWishlistContent(content: string): Promise { const prompt = ` -Summarize the wishlist into two ultra-short single-line fragments (<=120 chars each). -Return EXACTLY in this format, nothing else: -wants: -offers: -- Be terse, remove filler, prefer keywords over sentences. -- Avoid newlines, commas, and the delimiter "${DELIMITER}". -- If explicit wants/offers are missing, infer concise intent/skills from context. +Extract and summarize the wishlist into two arrays of short items. + +Return EXACTLY in this JSON format, nothing else: +{ + "wants": ["item 1", "item 2", "item 3"], + "offers": ["item 1", "item 2", "item 3"] +} + +Rules: +- Extract each distinct want/offer as a separate array item +- Each item should be a short phrase (5-30 characters) +- Be terse, remove filler, prefer keywords over sentences +- Avoid the delimiter "${DELIMITER}" in items +- If explicit wants/offers are missing, infer concise intent/skills from context +- Return empty arrays [] if no meaningful content found +- Maximum 10 items per array `.trim(); try { @@ -47,7 +56,7 @@ offers: messages: [ { role: "system", - content: "You are a terse summarizer that emits exactly two lines: wants and offers.", + content: "You are a summarizer that extracts wants and offers as JSON arrays of short phrases.", }, { role: "user", @@ -55,7 +64,21 @@ offers: }, ], temperature: 0.2, - max_tokens: 180, + max_tokens: 500, + response_format: { type: "json_object" }, + }).catch((error: any) => { + console.error("WishlistSummaryService OpenAI API Error:"); + console.error(" Error type:", error?.constructor?.name); + console.error(" Error message:", error?.message); + console.error(" Error code:", error?.code); + console.error(" Error status:", error?.status); + + if (error?.status === 429 || error?.code === 'rate_limit_exceeded' || error?.message?.includes('rate limit')) { + console.error("RATE LIMIT DETECTED in WishlistSummaryService!"); + console.error(" Rate limit error details:", JSON.stringify(error, null, 2)); + } + + throw error; }); const raw = response.choices[0]?.message?.content || ""; @@ -67,11 +90,11 @@ offers: console.error("WishlistSummaryService: OpenAI summarization failed, using fallback", error); } - // Fallback: truncate raw content into two identical concise lines + // Fallback: extract basic items from content const fallback = this.buildFallback(content); return { - summaryWants: fallback, - summaryOffers: fallback, + summaryWants: fallback.wants, + summaryOffers: fallback.offers, }; } @@ -83,7 +106,8 @@ offers: } async ensureSummaries(wishlist: Wishlist): Promise { - if (wishlist.summaryWants && wishlist.summaryOffers) { + if (wishlist.summaryWants && wishlist.summaryWants.length > 0 && + wishlist.summaryOffers && wishlist.summaryOffers.length > 0) { return wishlist; } return this.summarizeAndPersist(wishlist); @@ -117,39 +141,141 @@ offers: console.log("WishlistSummaryService: Backfill completed"); } - private parseSummary(raw: string): WishlistSummary | null { - const cleaned = raw.replace(/\r/g, "").trim(); - const wantsMatch = cleaned.match(/wants:\s*(.*)/i); - const offersMatch = cleaned.match(/offers:\s*(.*)/i); + /** + * Summarize all wishlists and log the results + * Used for comprehensive summarization on platform start + */ + async summarizeAllWishlists(): Promise { + const allWishlists = await this.wishlistRepository.find({ + where: { isActive: true }, + order: { updatedAt: "DESC" }, + }); - if (!wantsMatch && !offersMatch) { - return null; + if (allWishlists.length === 0) { + console.log("WishlistSummaryService: No active wishlists to summarize"); + return; } - const summaryWants = this.cleanFragment(wantsMatch?.[1] || ""); - const summaryOffers = this.cleanFragment(offersMatch?.[1] || ""); + console.log(`\n${"=".repeat(80)}`); + console.log(`WishlistSummaryService: Starting comprehensive summarization of ${allWishlists.length} wishlists`); + console.log(`${"=".repeat(80)}\n`); - if (!summaryWants && !summaryOffers) { - return null; + let successCount = 0; + let errorCount = 0; + + for (const wishlist of allWishlists) { + try { + const summary = await this.summarizeWishlistContent(wishlist.content); + + // Log the raw content and summary + console.log(`[${wishlist.id}]`); + console.log(` Raw: ${wishlist.content.substring(0, 200)}${wishlist.content.length > 200 ? '...' : ''}`); + console.log(` Summary Wants: ${JSON.stringify(summary.summaryWants)}`); + console.log(` Summary Offers: ${JSON.stringify(summary.summaryOffers)}`); + console.log(''); + + // Persist the summary + wishlist.summaryWants = summary.summaryWants; + wishlist.summaryOffers = summary.summaryOffers; + await this.wishlistRepository.save(wishlist); + + successCount++; + } catch (error) { + console.error(`[${wishlist.id}] Failed to summarize:`, error); + errorCount++; + } } - return { - summaryWants: summaryWants || summaryOffers || "", - summaryOffers: summaryOffers || summaryWants || "", - }; + console.log(`${"=".repeat(80)}`); + console.log(`WishlistSummaryService: Summarization completed`); + console.log(` Success: ${successCount}`); + console.log(` Errors: ${errorCount}`); + console.log(`${"=".repeat(80)}\n`); + } + + private parseSummary(raw: string): WishlistSummary | null { + try { + const cleaned = raw.replace(/\r/g, "").trim(); + const jsonMatch = cleaned.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return null; + } + + const parsed = JSON.parse(jsonMatch[0]); + const wants = Array.isArray(parsed.wants) ? parsed.wants : []; + const offers = Array.isArray(parsed.offers) ? parsed.offers : []; + + // Clean and validate items + const cleanWants = wants + .filter((item: any) => typeof item === 'string' && item.trim().length > 0) + .map((item: string) => this.cleanArrayItem(item)) + .filter((item: string) => item.length > 0 && item.length <= 100) + .slice(0, 10); + + const cleanOffers = offers + .filter((item: any) => typeof item === 'string' && item.trim().length > 0) + .map((item: string) => this.cleanArrayItem(item)) + .filter((item: string) => item.length > 0 && item.length <= 100) + .slice(0, 10); + + return { + summaryWants: cleanWants.length > 0 ? cleanWants : [], + summaryOffers: cleanOffers.length > 0 ? cleanOffers : [], + }; + } catch (error) { + console.error("WishlistSummaryService: Failed to parse summary JSON", error); + return null; + } } - private cleanFragment(fragment: string): string { - return fragment + private cleanArrayItem(item: string): string { + return item .replace(new RegExp(DELIMITER, "g"), " ") .replace(/\s+/g, " ") .replace(/[,\n\r]+/g, " ") .trim() - .slice(0, MAX_FALLBACK_LENGTH); + .slice(0, 100); } - private buildFallback(content: string): string { - return this.cleanFragment(content).slice(0, MAX_FALLBACK_LENGTH); + private buildFallback(content: string): { wants: string[]; offers: string[] } { + // Extract list items from markdown content as fallback + const wants: string[] = []; + const offers: string[] = []; + + const lines = content.split('\n'); + let currentSection: 'wants' | 'offers' | null = null; + + for (const line of lines) { + const trimmed = line.trim(); + + // Detect section headers + if (/^##\s*what\s+i\s+want/i.test(trimmed)) { + currentSection = 'wants'; + continue; + } + if (/^##\s*what\s+i\s+can\s+do/i.test(trimmed)) { + currentSection = 'offers'; + continue; + } + + // Extract list items + const listItemMatch = trimmed.match(/^[-*]\s+(.+)$/); + if (listItemMatch) { + const item = this.cleanArrayItem(listItemMatch[1]); + if (item.length > 0) { + if (currentSection === 'wants') { + wants.push(item); + } else if (currentSection === 'offers') { + offers.push(item); + } + } + } + } + + return { + wants: wants.slice(0, 10), + offers: offers.slice(0, 10), + }; } }