pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
426 lines (407 loc) • 19.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const openai_1 = __importDefault(require("openai"));
const generative_ai_1 = require("@google/generative-ai");
const llm_base_1 = require("./llm-base");
const env_loader_1 = require("../config/env-loader");
const firebase_config_1 = require("../config/firebase-config");
const pulse_type_registry_1 = require("pulse-type-registry");
const content_types_config_1 = require("../config/content-types-config");
// Constants for Gemini models
const GEMINI_WEB_SEARCH_MODEL = 'gemini-2.5-flash-preview-05-20'; // Model that supports web search
const GEMINI_STRUCTURED_OUTPUT_MODEL = 'gemini-1.5-flash'; // Model that supports structured output
/**
* Gemini Helper - Direct Google Gemini API integration
* Supports web search and structured output (in different models)
*/
class GeminiHelper extends llm_base_1.LLMBase {
constructor(apiKey, openaiInstance, model = GEMINI_WEB_SEARCH_MODEL) {
const config = {
model,
apiKey: apiKey || (0, env_loader_1.getEnvVar)('GEMINI_API_KEY'),
baseURL: 'https://generativelanguage.googleapis.com/v1beta'
};
super(config, openaiInstance);
// Initialize Google Generative AI client
this.genAI = new generative_ai_1.GoogleGenerativeAI(config.apiKey);
this.webSearchModel = this.genAI.getGenerativeModel({ model: GEMINI_WEB_SEARCH_MODEL });
this.structuredOutputModel = this.genAI.getGenerativeModel({ model: GEMINI_STRUCTURED_OUTPUT_MODEL });
}
/**
* Create Gemini helper with model from remote config
*/
static async createWithRemoteConfig(apiKey, openaiInstance) {
try {
const remoteModel = await (0, firebase_config_1.getGeminiModel)();
return new GeminiHelper(apiKey, openaiInstance, remoteModel);
}
catch (error) {
console.warn('Failed to fetch model from remote config, using defaults:', error);
return new GeminiHelper(apiKey, openaiInstance);
}
}
createOpenAIInstance(config) {
// Create a compatible OpenAI instance for base class compatibility
// This is not used for actual Gemini API calls
return new openai_1.default({
apiKey: config.apiKey || 'dummy-key-for-gemini',
baseURL: config.baseURL,
defaultHeaders: config.headers
});
}
getProviderName() {
return 'gemini';
}
/**
* Generate embeddings using Gemini
* Note: Gemini doesn't have a dedicated embedding model yet
*/
async generateEmbedding(text) {
throw new Error('Gemini does not support embeddings generation yet');
}
/**
* Fetch latest tweets for a given area using two-step Gemini process
* Step 1: Web search with Gemini Flash Exp (supports web search)
* Step 2: Structure the results with Gemini Flash (supports structured output)
*/
async fetchLatestTweets(area, region, country) {
try {
// Step 1: Web search for latest tweets
const searchPrompt = `
Search for the latest and most popular tweets and Twitter/X posts from ${area}, ${region}, ${country}.
Focus on:
1. Breaking news and current events
2. Trending topics and viral content
3. Important announcements from local authorities, businesses, or influencers
4. Community discussions and local happenings
5. Weather updates or emergency alerts if any
Please find the most recent and relevant tweets from the past 24-48 hours.
Include the tweet content, author information, engagement metrics if available, and direct links to the tweets.
Prioritize tweets with high engagement or from verified/notable accounts. Today's date is ${new Date().toISOString().split('T')[0]}
`;
console.log('Today\'s date:', new Date().toISOString().split('T')[0]);
console.log(`Fetching tweets for ${area}, ${region}, ${country} using Gemini web search...`);
// Use the web search model - Gemini 2.0 Flash Exp has built-in web grounding
// We'll use a prompt that encourages web search
const webSearchPrompt = `${searchPrompt}
Please search the web and Twitter/X for the most recent information. Use current web data to find real tweets from the last 24-48 hours. CRITICAL to have only tweets from the last 24-48 hours. Provide images if present`;
// Use proper Google Search grounding - cast to any to bypass type issues
const webSearchResult = await this.webSearchModel.generateContent({
contents: [{ role: 'user', parts: [{ text: webSearchPrompt }] }],
tools: [{ googleSearch: {} }],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 8192,
}
});
const webSearchText = webSearchResult.response.text();
console.log('Web search completed, structuring results...');
console.log('Web search raw result (first 500 chars):', webSearchText.substring(0, 500));
// Check for grounding metadata
try {
const groundingMetadata = webSearchResult.response.candidates?.[0]?.groundingMetadata;
if (groundingMetadata?.searchEntryPoint?.renderedContent) {
console.log('Grounding metadata available:', groundingMetadata.searchEntryPoint.renderedContent.substring(0, 200));
}
}
catch (e) {
console.log('No grounding metadata available');
}
// Step 2: Structure the results using structured output model
const structurePrompt = `
Based on the following web search results about tweets from ${area}, ${region}, ${country},
extract and structure the tweet information into a JSON array.
Web search results:
${webSearchText}
For each tweet found, extract:
- id: A unique identifier (can be derived from the URL or create one)
- title: The tweet text/content
- image_url: URL of any image in the tweet (null if none)
- profile_picture_url: The author's profile picture URL
- username: The Twitter username (without @)
- source: The display name of the tweet author
- source_url: The direct URL to the tweet
- date: The tweet timestamp in ISO format
Return only tweets that have actual content and valid source URLs.
`;
// Define the schema for structured output
const schema = {
type: generative_ai_1.SchemaType.ARRAY,
items: {
type: generative_ai_1.SchemaType.OBJECT,
properties: {
id: { type: generative_ai_1.SchemaType.STRING },
title: { type: generative_ai_1.SchemaType.STRING },
image_url: { type: generative_ai_1.SchemaType.STRING, nullable: true },
profile_picture_url: { type: generative_ai_1.SchemaType.STRING },
username: { type: generative_ai_1.SchemaType.STRING },
source: { type: generative_ai_1.SchemaType.STRING },
source_url: { type: generative_ai_1.SchemaType.STRING },
date: { type: generative_ai_1.SchemaType.STRING }
},
required: ['id', 'title', 'profile_picture_url', 'username', 'source', 'source_url', 'date']
}
};
// Get structured output
const structuredModel = this.genAI.getGenerativeModel({
model: GEMINI_STRUCTURED_OUTPUT_MODEL,
generationConfig: {
responseMimeType: "application/json",
responseSchema: schema
}
});
const structuredResult = await structuredModel.generateContent(structurePrompt);
const structuredText = structuredResult.response.text();
console.log('Structured result:', structuredText);
const tweets = JSON.parse(structuredText);
// Validate with Zod schema
const validatedTweets = tweets.map((tweet) => {
try {
return pulse_type_registry_1.TweetSchema.parse(tweet);
}
catch (error) {
console.warn(`Failed to validate tweet: ${JSON.stringify(tweet)}`, error);
return null;
}
}).filter(Boolean);
console.log(`Successfully fetched and structured ${validatedTweets.length} tweets`);
return validatedTweets;
}
catch (error) {
console.error('Error fetching tweets with Gemini:', error);
throw error;
}
}
/**
* Fetch local content (events, deals, news, reels, places) using two-step Gemini process
* Similar to runQuery and fetchStructuredDataFromWeb but using Gemini's web search
*/
async fetchLocalContent(area, region, country, categories) {
try {
// Determine which categories to search for
const enabledTypes = (0, content_types_config_1.getEnabledContentTypes)();
const searchCategories = categories && categories.length > 0
? categories.filter(cat => enabledTypes.includes(cat))
: enabledTypes.filter(type => type !== 'tweets'); // Exclude tweets from general content
// Step 1: Web search for local content
const searchPrompt = `
Search for the latest local content in ${area}, ${region}, ${country}.
Find the following types of content from the past week:
${searchCategories.includes('events') ? `
1. EVENTS: Local events, concerts, festivals, sports games, meetups, community gatherings
- Include: event name, date/time, location, description, ticket info if available
` : ''}
${searchCategories.includes('deals') ? `
2. DEALS: Local business deals, discounts, promotions, special offers
- Include: business name, deal description, discount percentage/amount, valid dates
` : ''}
${searchCategories.includes('news') ? `
3. NEWS: Local news, breaking stories, community updates, weather alerts
- Include: headline, summary, source, publication date
` : ''}
${searchCategories.includes('reels') ? `
4. REELS/VIDEOS: Popular local video content, TikToks, Instagram Reels about the area
- Include: video title, creator, platform, view count if available
` : ''}
${searchCategories.includes('places') ? `
5. PLACES: New restaurants, stores, attractions, popular local spots
- Include: place name, address, type of establishment, what makes it notable
` : ''}
Focus on content from reputable local sources, official websites, and verified social media accounts.
Prioritize recent and relevant information. Today's date is ${new Date().toISOString().split('T')[0]}.
`;
console.log(`Fetching local content for ${area}, ${region}, ${country}...`);
console.log('Categories:', searchCategories.join(', '));
// Use proper Google Search grounding
const webSearchResult = await this.webSearchModel.generateContent({
contents: [{ role: 'user', parts: [{ text: searchPrompt }] }],
tools: [{ googleSearch: {} }],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 8192,
}
});
const webSearchText = webSearchResult.response.text();
console.log('Web search completed, structuring results...');
// Check for grounding metadata
try {
const groundingMetadata = webSearchResult.response.candidates?.[0]?.groundingMetadata;
if (groundingMetadata?.searchEntryPoint?.renderedContent) {
console.log('Grounding metadata available');
}
}
catch (e) {
console.log('No grounding metadata available');
}
// Step 2: Structure the results using structured output model
const structurePrompt = `
Based on the following web search results about local content in ${area}, ${region}, ${country},
extract and structure the information into appropriate categories.
Web search results:
${webSearchText}
Structure each item according to its type:
FOR EVENTS - extract:
- id: unique identifier
- title: event name
- date: event date/time in ISO format
- location: venue/address
- description: event details
- image_url: event image/poster (null if none)
- source: where the info came from
- source_url: link to more info
- emoji: relevant emoji (🎭🎵🎨🏃♂️⚽️🎉)
- category: "events"
FOR DEALS - extract:
- id: unique identifier
- product: what's on sale
- title: deal headline
- discount: discount amount/percentage
- price: final price if available
- description: deal details
- source: business name
- source_url: link to deal
- image_url: product/deal image (null if none)
- timestamp: when deal was posted
- emoji: relevant emoji (💰🛍️🏷️🎯🔥)
- category: "deals"
FOR NEWS - extract:
- id: unique identifier
- title: news headline
- description: article summary
- source: news outlet
- source_url: article link
- image_url: article image (null if none)
- category: "news"
FOR REELS/VIDEOS - extract:
- id: unique identifier
- title: video title/description
- duration: video length
- thumbnail_url: video thumbnail
- source: creator/platform
- source_url: video link
- category: "reels"
FOR PLACES - extract:
- id: unique identifier
- name: place name
- address: full address
- description: what makes it notable
- type: restaurant/store/attraction/etc
- image_url: place image (null if none)
- source: where info came from
- source_url: link to more info
- category: "places"
Return as a JSON array with mixed content types. Each item MUST have a category field.
`;
// Create a union schema for structured output
const schemaProperties = {
id: { type: generative_ai_1.SchemaType.STRING },
category: {
type: generative_ai_1.SchemaType.STRING,
enum: searchCategories
}
};
// Add common fields
const commonFields = ['title', 'source', 'source_url', 'image_url', 'description'];
commonFields.forEach(field => {
schemaProperties[field] = {
type: generative_ai_1.SchemaType.STRING,
nullable: field === 'image_url' || field === 'description'
};
});
// Add type-specific fields as optional
schemaProperties.date = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.location = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.emoji = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.product = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.discount = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.price = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.timestamp = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.duration = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.thumbnail_url = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.name = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.address = { type: generative_ai_1.SchemaType.STRING, nullable: true };
schemaProperties.type = { type: generative_ai_1.SchemaType.STRING, nullable: true };
const schema = {
type: generative_ai_1.SchemaType.ARRAY,
items: {
type: generative_ai_1.SchemaType.OBJECT,
properties: schemaProperties,
required: ['id', 'category', 'title', 'source', 'source_url']
}
};
// Get structured output
const structuredModel = this.genAI.getGenerativeModel({
model: GEMINI_STRUCTURED_OUTPUT_MODEL,
generationConfig: {
responseMimeType: "application/json",
responseSchema: schema
}
});
const structuredResult = await structuredModel.generateContent(structurePrompt);
const structuredText = structuredResult.response.text();
console.log('Structured result received');
const items = JSON.parse(structuredText);
// Validate each item with the appropriate schema based on its category
const validatedItems = items.map((item) => {
try {
switch (item.category) {
case 'events':
return { ...pulse_type_registry_1.EventSchema.parse(item), category: 'events' };
case 'deals':
return { ...pulse_type_registry_1.DealSchema.parse(item), category: 'deals' };
case 'news':
return { ...pulse_type_registry_1.NewsArticleSchema.parse(item), category: 'news' };
case 'reels':
return { ...pulse_type_registry_1.ReelSchema.parse(item), category: 'reels' };
case 'places':
return { ...pulse_type_registry_1.PlaceSchema.parse(item), category: 'places' };
default:
console.warn(`Unknown category: ${item.category}`);
return null;
}
}
catch (error) {
console.warn(`Failed to validate item in category ${item.category}:`, error);
// Return the item anyway with its category
return { ...item, category: item.category || 'unknown' };
}
}).filter(Boolean);
console.log(`Successfully fetched ${validatedItems.length} local content items`);
return validatedItems;
}
catch (error) {
console.error('Error fetching local content with Gemini:', error);
throw error;
}
}
/**
* Override the base class method to use Gemini's native API
*/
async query(prompt, outputFormat, model, _systemPrompt) {
try {
const modelToUse = model || this.defaultModel;
const geminiModel = this.genAI.getGenerativeModel({ model: modelToUse });
const result = await geminiModel.generateContent(prompt);
const response = result.response.text();
if (outputFormat) {
// Try to parse as JSON if output format is provided
try {
return JSON.parse(response);
}
catch {
return response;
}
}
return response;
}
catch (error) {
console.error('Gemini query error:', error);
throw error;
}
}
}
exports.default = GeminiHelper;
//# sourceMappingURL=gemini-helper.js.map