Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 platforms/dreamsync-api/src/database/entities/Wishlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class Wishlist {
@Column({ type: "text" })
content: string; // Markdown content

@Column({ type: "text", nullable: true })
summaryWants: string | null;

@Column({ type: "text", nullable: true })
summaryOffers: string | null;

@Column({ type: "boolean", default: true })
isActive: boolean;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1765187375413 implements MigrationInterface {
name = 'Migration1765187375413'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "wishlists" ADD "summaryWants" text`);
await queryRunner.query(`ALTER TABLE "wishlists" ADD "summaryOffers" text`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "wishlists" DROP COLUMN "summaryOffers"`);
await queryRunner.query(`ALTER TABLE "wishlists" DROP COLUMN "summaryWants"`);
}

}
9 changes: 9 additions & 0 deletions platforms/dreamsync-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { authMiddleware, authGuard } from "./middleware/auth";
import { adapter } from "./web3adapter/watchers/subscriber";
import { MatchingJob } from "./services/MatchingJob";
import { PlatformEVaultService } from "./services/PlatformEVaultService";
import { WishlistSummaryService } from "./services/WishlistSummaryService";

config({ path: path.resolve(__dirname, "../../../.env") });

Expand Down Expand Up @@ -42,6 +43,14 @@ AppDataSource.initialize()
// Don't exit the process, just log the error
}

// Backfill wishlist summaries for existing records
try {
const wishlistSummaryService = WishlistSummaryService.getInstance();
await wishlistSummaryService.backfillMissingSummaries();
} catch (error) {
console.error("❌ Failed to backfill wishlist summaries:", error);
}

// Start AI matching job (disabled automatic startup)
const matchingJob = new MatchingJob();
// matchingJob.start(60); // Run every 60 minutes - DISABLED
Expand Down
18 changes: 18 additions & 0 deletions platforms/dreamsync-api/src/services/AIMatchingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MatchNotificationService } from "./MatchNotificationService";
import { MatchingService, MatchResult, WishlistData, GroupData } from "./MatchingService";
import { withOperationContext } from "../context/OperationContext";
import OpenAI from "openai";
import { WishlistSummaryService } from "./WishlistSummaryService";

export class AIMatchingService {
private matchingService: MatchingService;
Expand All @@ -15,6 +16,7 @@ export class AIMatchingService {
private groupRepository: Repository<Group>;
private notificationService: MatchNotificationService;
private openai: OpenAI;
private wishlistSummaryService: WishlistSummaryService;

constructor() {
this.matchingService = new MatchingService();
Expand All @@ -25,6 +27,7 @@ export class AIMatchingService {
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
this.wishlistSummaryService = WishlistSummaryService.getInstance();
}

async findMatches(): Promise<void> {
Expand All @@ -36,6 +39,7 @@ export class AIMatchingService {
return withOperationContext('AIMatchingService', operationId, async () => {
const wishlists = await this.getWishlistsForMatching();
console.log(`📋 Found ${wishlists.length} wishlists to analyze`);
await this.ensureWishlistSummaries(wishlists);

// Get existing groups for context
const existingGroups = await this.getExistingGroups();
Expand All @@ -45,6 +49,8 @@ export class AIMatchingService {
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,
Expand Down Expand Up @@ -216,6 +222,18 @@ export class AIMatchingService {
}));
}

private async ensureWishlistSummaries(wishlists: Wishlist[]): Promise<void> {
for (const wishlist of wishlists) {
if (!wishlist.summaryWants || !wishlist.summaryOffers) {
try {
await this.wishlistSummaryService.ensureSummaries(wishlist);
} catch (error) {
console.error(`Failed to ensure summary for wishlist ${wishlist.id}`, error);
}
}
}
}

private extractActivityCategory(groupName: string): string {
const name = groupName.toLowerCase();
if (name.includes('chess')) return 'chess';
Expand Down
36 changes: 29 additions & 7 deletions platforms/dreamsync-api/src/services/MatchingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface MatchResult {
export interface WishlistData {
id: string;
content: string;
summaryWants: string;
summaryOffers: string;
userId: string;
user: {
id: string;
Expand Down Expand Up @@ -64,12 +66,18 @@ export class MatchingService {
}

private buildAllMatchesPrompt(wishlists: WishlistData[], existingGroups?: GroupData[]): string {
const wishlistTexts = wishlists.map((wishlist, index) =>
`Wishlist ${index + 1} (User: ${wishlist.user.name}, ID: ${wishlist.userId}):
${wishlist.content}

---`
).join('\n\n');
const delimiter = "<|>";
const header = `userEname${delimiter}userName${delimiter}wants${delimiter}offers`;
const rows = wishlists.map((wishlist) => {
const wants = wishlist.summaryWants || wishlist.content;
const offers = wishlist.summaryOffers || wishlist.content;
return [
this.sanitizeField(wishlist.user.ename),
this.sanitizeField(wishlist.user.name || wishlist.user.ename),
this.sanitizeField(wants),
this.sanitizeField(offers),
].join(delimiter);
}).join("\n");

let existingGroupsText = '';
if (existingGroups && existingGroups.length > 0) {
Expand Down Expand Up @@ -102,7 +110,13 @@ However, if users want to collaborate on a DIFFERENT activity than existing grou
return `
You are an AI matching assistant. Analyze ALL the wishlists below and find meaningful connections between users.

${wishlistTexts}${existingGroupsText}
The wishlists are provided as delimiter-separated rows (delimiter: "${delimiter}").
Columns: userEname${delimiter}userName${delimiter}wants${delimiter}offers

${header}
${rows}

Use ONLY the summary rows above (not full prose) to infer matches.${existingGroupsText}

TASK: Find ALL meaningful matches between these users based on their wishlists.

Expand Down Expand Up @@ -154,6 +168,14 @@ Be thorough and find ALL potential matches!
`.trim();
}

private sanitizeField(value?: string): string {
if (!value) return "";
return value
.replace(/<\|>/g, " ")
.replace(/\s+/g, " ")
.trim();
}

private async analyzeAllMatches(wishlists: WishlistData[], existingGroups?: GroupData[]): Promise<MatchResult[]> {
const prompt = this.buildAllMatchesPrompt(wishlists, existingGroups);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface MatchResult {
export interface WishlistData {
id: string;
content: string;
summaryWants: string;
summaryOffers: string;
userId: string;
user: {
id: string;
Expand Down
7 changes: 5 additions & 2 deletions platforms/dreamsync-api/src/services/WishlistService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { AppDataSource } from "../database/data-source";
import { Wishlist } from "../database/entities/Wishlist";
import { User } from "../database/entities/User";
import { Repository } from "typeorm";
import { WishlistSummaryService } from "./WishlistSummaryService";

export class WishlistService {
private wishlistRepository: Repository<Wishlist>;
private userRepository: Repository<User>;
private wishlistSummaryService: WishlistSummaryService;

constructor() {
this.wishlistRepository = AppDataSource.getRepository(Wishlist);
this.userRepository = AppDataSource.getRepository(User);
this.wishlistSummaryService = WishlistSummaryService.getInstance();
}

async createWishlist(
Expand All @@ -35,7 +38,7 @@ export class WishlistService {
}
});

return await this.wishlistRepository.save(wishlist);
return await this.wishlistSummaryService.summarizeAndPersist(wishlist);
}

async updateWishlist(
Expand All @@ -58,7 +61,7 @@ export class WishlistService {
}

Object.assign(wishlist, updates);
return await this.wishlistRepository.save(wishlist);
return await this.wishlistSummaryService.summarizeAndPersist(wishlist);
}

async getUserWishlists(userId: string): Promise<Wishlist[]> {
Expand Down
155 changes: 155 additions & 0 deletions platforms/dreamsync-api/src/services/WishlistSummaryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import OpenAI from "openai";
import { Repository, IsNull } from "typeorm";
import { AppDataSource } from "../database/data-source";
import { Wishlist } from "../database/entities/Wishlist";

type WishlistSummary = {
summaryWants: string;
summaryOffers: string;
};

const DELIMITER = "<|>";
const MAX_FALLBACK_LENGTH = 140;

export class WishlistSummaryService {
private static instance: WishlistSummaryService | null = null;
private wishlistRepository: Repository<Wishlist>;
private openai: OpenAI;

private constructor() {
this.wishlistRepository = AppDataSource.getRepository(Wishlist);
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}

static getInstance(): WishlistSummaryService {
if (!this.instance) {
this.instance = new WishlistSummaryService();
}
return this.instance;
}

async summarizeWishlistContent(content: string): Promise<WishlistSummary> {
const prompt = `
Summarize the wishlist into two ultra-short single-line fragments (<=120 chars each).
Return EXACTLY in this format, nothing else:
wants: <succinct wants summary>
offers: <succinct offers summary>
- 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.
`.trim();

try {
const response = await this.openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "You are a terse summarizer that emits exactly two lines: wants and offers.",
},
{
role: "user",
content: `${prompt}\n\nWishlist content:\n${content}`,
},
],
temperature: 0.2,
max_tokens: 180,
});

const raw = response.choices[0]?.message?.content || "";
const parsed = this.parseSummary(raw);
if (parsed) {
return parsed;
}
} catch (error) {
console.error("WishlistSummaryService: OpenAI summarization failed, using fallback", error);
}

// Fallback: truncate raw content into two identical concise lines
const fallback = this.buildFallback(content);
return {
summaryWants: fallback,
summaryOffers: fallback,
};
}

async summarizeAndPersist(wishlist: Wishlist): Promise<Wishlist> {
const { summaryWants, summaryOffers } = await this.summarizeWishlistContent(wishlist.content);
wishlist.summaryWants = summaryWants;
wishlist.summaryOffers = summaryOffers;
return this.wishlistRepository.save(wishlist);
}

async ensureSummaries(wishlist: Wishlist): Promise<Wishlist> {
if (wishlist.summaryWants && wishlist.summaryOffers) {
return wishlist;
}
return this.summarizeAndPersist(wishlist);
}

async backfillMissingSummaries(batchSize: number = 25): Promise<void> {
const missing = await this.wishlistRepository.find({
where: [
{ summaryWants: IsNull() },
{ summaryOffers: IsNull() },
],
order: { updatedAt: "DESC" },
take: batchSize,
});

if (missing.length === 0) {
console.log("WishlistSummaryService: No wishlists missing summaries");
return;
}

console.log(`WishlistSummaryService: Backfilling summaries for ${missing.length} wishlists...`);

for (const wishlist of missing) {
try {
await this.summarizeAndPersist(wishlist);
} catch (error) {
console.error(`WishlistSummaryService: Failed to summarize wishlist ${wishlist.id}`, error);
}
}

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);

if (!wantsMatch && !offersMatch) {
return null;
}

const summaryWants = this.cleanFragment(wantsMatch?.[1] || "");
const summaryOffers = this.cleanFragment(offersMatch?.[1] || "");

if (!summaryWants && !summaryOffers) {
return null;
}

return {
summaryWants: summaryWants || summaryOffers || "",
summaryOffers: summaryOffers || summaryWants || "",
};
}

private cleanFragment(fragment: string): string {
return fragment
.replace(new RegExp(DELIMITER, "g"), " ")
.replace(/\s+/g, " ")
.replace(/[,\n\r]+/g, " ")
.trim()
.slice(0, MAX_FALLBACK_LENGTH);
}

private buildFallback(content: string): string {
return this.cleanFragment(content).slice(0, MAX_FALLBACK_LENGTH);
}
}