Created
March 21, 2025 21:34
-
-
Save brandonbryant12/14d79c506ceae35d53c1304b532a0848 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server/lib/azure-openai.ts | |
import dotenv from 'dotenv'; | |
import OpenAI from "openai"; | |
import { encode, decode } from "gpt-3-encoder"; | |
// Load environment variables from .env file | |
dotenv.config(); | |
interface TokenCache { | |
token: string; | |
expiresAt: number; | |
} | |
let tokenCache: TokenCache | null = null; | |
export function getAzureOpenAIClient() { | |
// Create the custom fetch handler for authentication if needed | |
const customFetch = async ( | |
url: RequestInfo | URL, | |
init?: RequestInit | |
) => { | |
// Get the authentication token if needed | |
// This can be replaced with your actual auth token retrieval logic | |
const token = await getAuthToken(); | |
// Add the token to the Authorization header if using AAD auth | |
// Otherwise, the API key is used directly in the client config | |
const headers = { | |
...init?.headers, | |
"Authorization": `Bearer ${token}` | |
}; | |
// Return the fetch with added headers | |
return fetch(url, { | |
...init, | |
headers | |
}); | |
}; | |
// Create the Azure OpenAI client | |
const client = new OpenAI({ | |
apiKey: process.env.AZURE_OPENAI_API_KEY, | |
baseURL: `${process.env.AZURE_OPENAI_ENDPOINT}/openai/deployments/${process.env.AZURE_OPENAI_DEPLOYMENT}`, | |
defaultQuery: { "api-version": process.env.AZURE_OPENAI_API_VERSION || "2024-02-01" }, | |
fetch: customFetch, // Only include if using AAD token authentication | |
maxRetries: 3, | |
timeout: 60000, // 60 second timeout | |
}); | |
return client; | |
} | |
async function getAuthToken(): Promise<string> { | |
const now = Date.now(); | |
// Check if we have a valid cached token | |
if (tokenCache && now < tokenCache.expiresAt) { | |
return tokenCache.token; | |
} | |
// If no valid token in cache, fetch a new one | |
// This is where you'd make the actual token fetch request | |
// For now, we're using a dummy implementation | |
const newToken = "dummy-auth-token"; | |
// Cache the token with a TTL of 55 minutes (in milliseconds) | |
const ttlMs = 55 * 60 * 1000; | |
tokenCache = { | |
token: newToken, | |
expiresAt: now + ttlMs | |
}; | |
return newToken; | |
} | |
// Utility functions for content management | |
export function countTokens(text: string): number { | |
return encode(text).length; | |
} | |
export function chunkContent(text: string, maxTokens: number = 2000): string[] { | |
const tokens = encode(text); | |
const chunks: string[] = []; | |
let currentChunk: number[] = []; | |
for (let i = 0; i < tokens.length; i++) { | |
currentChunk.push(tokens[i]); | |
if (currentChunk.length >= maxTokens) { | |
// Find the last period to make clean breaks | |
const text = decodeTokens(currentChunk); | |
const lastPeriod = text.lastIndexOf('.'); | |
if (lastPeriod !== -1) { | |
chunks.push(text.substring(0, lastPeriod + 1)); | |
// Keep the remainder for next chunk | |
const remainder = text.substring(lastPeriod + 1); | |
currentChunk = encode(remainder); | |
} else { | |
chunks.push(text); | |
currentChunk = []; | |
} | |
} | |
} | |
if (currentChunk.length > 0) { | |
chunks.push(decodeTokens(currentChunk)); | |
} | |
return chunks; | |
} | |
function decodeTokens(tokens: number[]): string { | |
return decode(tokens); | |
} | |
// You could also add a timeout wrapper for API calls | |
export async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> { | |
const timeout = new Promise<never>((_, reject) => | |
setTimeout(() => reject(new Error('Request timed out')), timeoutMs) | |
); | |
return Promise.race([promise, timeout]); | |
} | |
// Error handling for OpenAI API calls | |
export function handleOpenAIError(error: any): never { | |
console.error("OpenAI API Error:", error); | |
if (error?.message?.includes('Request timed out')) { | |
throw new Error( | |
"Request to Azure OpenAI API timed out. Please try again." | |
); | |
} | |
if (error?.status === 429 || | |
error?.error?.type === "insufficient_quota" || | |
error?.error?.code === "billing_hard_limit_reached") { | |
throw new Error( | |
"Azure OpenAI API quota exceeded. Please try again later or contact support." | |
); | |
} | |
if (error?.code === "ECONNRESET" || error?.code === "ETIMEDOUT") { | |
throw new Error( | |
"Connection error while processing content. Please try again." | |
); | |
} | |
throw error; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server/lib/openai.ts | |
import dotenv from 'dotenv'; | |
// Load environment variables from .env file | |
dotenv.config(); | |
import OpenAI from "openai"; | |
import { z } from "zod"; | |
import axios from "axios"; | |
import { transcriptSchema } from "@shared/schema"; | |
import { encode, decode } from "gpt-3-encoder"; | |
import { getAzureOpenAIClient } from './getOpenAIClient'; | |
const openai = getAzureOpenAIClient(); | |
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> { | |
const timeout = new Promise<never>((_, reject) => | |
setTimeout(() => reject(new Error('Request timed out')), timeoutMs) | |
); | |
return Promise.race([promise, timeout]); | |
} | |
export type VoiceType = "ash" | "sage"; | |
export async function generateSpeech(text: string, voice: VoiceType) { | |
const maxRetries = 3; | |
let retryCount = 0; | |
let lastError; | |
while (retryCount < maxRetries) { | |
try { | |
const voiceMapping = { | |
ash: "onyx" as const, // Using a more stable voice | |
sage: "nova" as const, // Using a more stable voice | |
}; | |
const response = await openai.audio.speech.create({ | |
model: "tts-1", | |
voice: voiceMapping[voice], | |
input: text, | |
response_format: "mp3", | |
}); | |
const buffer = Buffer.from(await response.arrayBuffer()); | |
return buffer; | |
} catch (error: any) { | |
lastError = error; | |
retryCount++; | |
// If it's a final retry, or if it's a non-retryable error, throw immediately | |
if (retryCount === maxRetries || | |
(error.status && error.status !== 408 && error.status !== 500 && error.status !== 503)) { | |
break; | |
} | |
// Exponential backoff | |
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000)); | |
} | |
} | |
handleOpenAIError(lastError); | |
} | |
class OpenAIQuotaError extends Error { | |
constructor(message: string) { | |
super(message); | |
this.name = "OpenAIQuotaError"; | |
} | |
} | |
function handleOpenAIError(error: any): never { | |
if (!process.env.AZURE_OPENAI_API_KEY) { | |
throw new Error( | |
"Azure OpenAI API key is not configured. Please check your environment variables.", | |
); | |
} | |
console.error("OpenAI API Error:", error); | |
if (error?.message?.includes('Request timed out')) { | |
throw new Error( | |
"Request to Azure OpenAI API timed out. Please try again.", | |
); | |
} | |
if (error?.status === 429 || | |
error?.error?.type === "insufficient_quota" || | |
error?.error?.code === "billing_hard_limit_reached") { | |
throw new OpenAIQuotaError( | |
"Azure OpenAI API quota exceeded. Please try again later or contact support.", | |
); | |
} | |
if (error?.code === "ECONNRESET" || error?.code === "ETIMEDOUT") { | |
throw new Error( | |
"Connection error while processing content. Please try again.", | |
); | |
} | |
throw error; | |
} | |
// Add new utility functions for content chunking | |
function countTokens(text: string): number { | |
return encode(text).length; | |
} | |
function chunkContent(text: string, maxTokens: number = 2000): string[] { | |
const tokens = encode(text); | |
const chunks: string[] = []; | |
let currentChunk: number[] = []; | |
// Add overlap to maintain context between chunks | |
const overlap = 100; | |
for (let i = 0; i < tokens.length; i++) { | |
currentChunk.push(tokens[i]); | |
if (currentChunk.length >= maxTokens) { | |
// Find the last period to make clean breaks | |
const text = decodeTokens(currentChunk); | |
const lastPeriod = text.lastIndexOf('.'); | |
if (lastPeriod !== -1) { | |
chunks.push(text.substring(0, lastPeriod + 1)); | |
// Keep the remainder for next chunk, including overlap | |
const remainder = text.substring(lastPeriod + 1); | |
currentChunk = encode(remainder); | |
} else { | |
chunks.push(text); | |
currentChunk = []; | |
} | |
} | |
} | |
if (currentChunk.length > 0) { | |
chunks.push(decodeTokens(currentChunk)); | |
} | |
return chunks; | |
} | |
// Fix: Changed name from 'decode' to 'decodeTokens' to avoid conflict with imported function | |
function decodeTokens(tokens: number[]): string { | |
return decode(tokens); | |
} | |
// The newest OpenAI model is "gpt-4o" which was released May 13, 2024 | |
const CHAT_MODEL = "gpt-4o"; | |
export async function generateSummary(text: string, customPrompt?: string) { | |
try { | |
const totalTokens = countTokens(text); | |
if (totalTokens > 800000) { | |
throw new Error(`Content is too large (${totalTokens} tokens). Maximum allowed is 800000 tokens.`); | |
} | |
// If content is large, chunk it and process sequentially | |
if (totalTokens > 2000) { | |
console.log(`Content is large (${totalTokens} tokens), processing in chunks...`); | |
const chunks = chunkContent(text); | |
const summaries = await Promise.all( | |
chunks.map(chunk => withTimeout(processChunk(chunk, customPrompt), 30000)) | |
); | |
// Combine summaries | |
return combineSummaries(summaries); | |
} | |
// For small content, process normally | |
return await withTimeout(processChunk(text, customPrompt), 30000); | |
} catch (error) { | |
console.error("Summary generation error:", error); | |
handleOpenAIError(error); | |
} | |
} | |
async function processChunk(text: string, customPrompt?: string) { | |
const systemPrompt = customPrompt | |
? `You are an expert content summarizer. ${customPrompt}. Provide response in JSON format with a non-empty title field and topics array.` | |
: `You are an expert content summarizer. Provide a concise summary of this section in JSON format with these fields: | |
- title: A clear, descriptive title for this section (REQUIRED, must not be empty) | |
- summary: A 2-3 sentence summary of the main points | |
- duration: Estimated reading time in minutes | |
- topics: Array of 2-4 main topics covered (REQUIRED, must be non-empty array of strings) | |
Each topic should be a clear, actionable item starting with a verb (e.g., "Understand market trends", "Review financial statements"). | |
Return the response as a JSON object.`; | |
const response = await openai.chat.completions.create({ | |
model: process.env.AZURE_OPENAI_DEPLOYMENT || CHAT_MODEL, | |
messages: [ | |
{ | |
role: "system", | |
content: systemPrompt, | |
}, | |
{ | |
role: "user", | |
content: text, | |
}, | |
], | |
response_format: { type: "json_object" }, | |
}); | |
const content = response.choices[0].message.content; | |
if (!content) { | |
throw new Error("Failed to generate summary: Empty response from OpenAI"); | |
} | |
console.log("Raw OpenAI response for summary:", content); | |
const parsedContent = JSON.parse(content); | |
console.log("Parsed topics before validation:", parsedContent.topics); | |
// Validate the required title field | |
if (!parsedContent.title || typeof parsedContent.title !== 'string' || parsedContent.title.trim() === '') { | |
parsedContent.title = "Content Summary"; | |
console.log("Warning: Missing or invalid title, using default title"); | |
} | |
// Process topics array, providing defaults if needed | |
if (!Array.isArray(parsedContent.topics)) { | |
console.log("Warning: Topics is not an array, creating default topics"); | |
parsedContent.topics = ["Understand the main concepts", "Review key points", "Apply the knowledge"]; | |
} else { | |
// Filter out invalid topics | |
const validTopics = parsedContent.topics | |
.filter((topic: any) => typeof topic === 'string' && topic.trim() !== '') | |
.map((topic: string) => topic.trim()); | |
// If no valid topics remain, provide defaults instead of throwing an error | |
if (validTopics.length === 0) { | |
console.log("Warning: No valid topics found, using default topics"); | |
parsedContent.topics = ["Understand the main concepts", "Review key points", "Apply the knowledge"]; | |
} else { | |
parsedContent.topics = validTopics; | |
} | |
} | |
console.log("Topics after validation:", parsedContent.topics); | |
// Ensure summary is valid | |
if (!parsedContent.summary || typeof parsedContent.summary !== 'string' || parsedContent.summary.trim() === '') { | |
parsedContent.summary = "A summary of the content highlighting key points and concepts."; | |
console.log("Warning: Missing or invalid summary, using default summary"); | |
} | |
// Ensure duration is valid | |
if (typeof parsedContent.duration !== 'number' || isNaN(parsedContent.duration)) { | |
parsedContent.duration = Math.ceil(text.length / 1500); // Rough estimate: ~1500 chars per minute | |
console.log("Warning: Missing or invalid duration, using estimated duration"); | |
} | |
return parsedContent; | |
} | |
function combineSummaries(summaries: any[]): any { | |
return { | |
title: summaries[0].title, | |
summary: summaries.map(s => s.summary).join(' '), | |
duration: summaries.reduce((acc, s) => acc + s.duration, 0), | |
topics: Array.from(new Set(summaries.flatMap(s => s.topics))), | |
}; | |
} | |
export async function generateDialogue(text: string, customPrompt?: string) { | |
try { | |
const totalTokens = countTokens(text); | |
if (totalTokens > 800000) { | |
throw new Error(`Content is too large (${totalTokens} tokens). Maximum allowed is 800000 tokens.`); | |
} | |
// For large content, process in chunks | |
if (totalTokens > 2000) { | |
console.log(`Content is large (${totalTokens} tokens), processing dialogue in chunks...`); | |
const chunks = chunkContent(text); | |
const dialogues = await Promise.all( | |
chunks.map(chunk => withTimeout(processDialogueChunk(chunk, customPrompt), 30000)) | |
); | |
// Combine dialogue segments | |
return combineDialogues(dialogues); | |
} | |
// For small content, process normally | |
return await withTimeout(processDialogueChunk(text, customPrompt), 30000); | |
} catch (error) { | |
console.error("Dialogue generation error:", error); | |
handleOpenAIError(error); | |
} | |
} | |
async function processDialogueChunk(text: string, customPrompt?: string) { | |
const baseSystemPrompt = `You are a master podcast scriptwriter specializing in creating highly engaging, natural conversations. ${customPrompt || "Transform this section of content into an emotionally rich dialogue segment"} | |
Return the response in this JSON format: | |
{ | |
"segments": [ | |
{ | |
"speaker": "string", // either "Ash" or "Sage" | |
"text": "string", // MUST include emotional markers | |
"timestamp": number // increment by 5-10 seconds per segment | |
} | |
] | |
}`; | |
const response = await openai.chat.completions.create({ | |
model: process.env.AZURE_OPENAI_DEPLOYMENT || CHAT_MODEL, | |
messages: [ | |
{ | |
role: "system", | |
content: baseSystemPrompt, | |
}, | |
{ | |
role: "user", | |
content: text, | |
}, | |
], | |
response_format: { type: "json_object" }, | |
}); | |
const content = response.choices[0].message.content; | |
if (!content) { | |
throw new Error("Failed to generate dialogue: Empty response from OpenAI"); | |
} | |
return transcriptSchema.parse(JSON.parse(content)); | |
} | |
function combineDialogues(dialogues: any[]): any { | |
let timestamp = 0; | |
const combinedSegments = dialogues.flatMap(dialogue => { | |
const segments = dialogue.segments.map((segment: any) => ({ | |
...segment, | |
timestamp: timestamp + segment.timestamp, | |
})); | |
timestamp = segments[segments.length - 1].timestamp; | |
return segments; | |
}); | |
return { | |
segments: combinedSegments, | |
}; | |
} | |
export async function translateDialogue( | |
transcript: { | |
segments: Array<{ speaker: string; text: string; timestamp: number }>; | |
}, | |
targetLanguage: string, | |
) { | |
try { | |
const response = await openai.chat.completions.create({ | |
model: process.env.AZURE_OPENAI_DEPLOYMENT || CHAT_MODEL, | |
messages: [ | |
{ | |
role: "system", | |
content: `You are an expert translator. Translate the given podcast transcript to ${targetLanguage} while maintaining the natural conversational flow, emotional markers, and personality traits. Keep the same speakers and timing, only translate the text. | |
Return the response in this JSON format: | |
{ | |
"segments": [ | |
{ | |
"speaker": "string", // keep original speaker names | |
"text": "string", // translated text including emotional markers | |
"timestamp": number // keep original timestamps | |
} | |
] | |
}`, | |
}, | |
{ | |
role: "user", | |
content: JSON.stringify(transcript), | |
}, | |
], | |
response_format: { type: "json_object" }, | |
}); | |
const content = response.choices[0].message.content; | |
if (!content) { | |
throw new Error( | |
"Failed to translate dialogue: Empty response from OpenAI", | |
); | |
} | |
return transcriptSchema.parse(JSON.parse(content)); | |
} catch (error) { | |
handleOpenAIError(error); | |
} | |
} | |
export async function generateQuizQuestions(transcript: { | |
segments: Array<{ speaker: string; text: string; timestamp: number }>; | |
}) { | |
try { | |
const response = await openai.chat.completions.create({ | |
model: process.env.AZURE_OPENAI_DEPLOYMENT || CHAT_MODEL, | |
messages: [ | |
{ | |
role: "system", | |
content: `You are an expert educator specializing in creating engaging quiz questions. Generate thought-provoking questions based on the podcast transcript. | |
Guidelines for questions: | |
1. Create questions that test understanding, not just recall | |
2. Include interesting wrong options that seem plausible | |
3. Provide helpful explanations for the correct answers | |
4. Reference specific timestamps from the content | |
5. Mix different types of questions: | |
- Main concept understanding | |
- Detail comprehension | |
- Application of ideas | |
- Critical thinking | |
Return the response in this JSON format: | |
{ | |
"questions": [ | |
{ | |
"question": string, // The actual question | |
"options": string[], // Array of 4 possible answers | |
"correctAnswer": number, // Index of correct answer (0-3) | |
"explanation": string, // Detailed explanation of why this is correct | |
"timestamp": number // Timestamp in podcast where this is discussed | |
} | |
] | |
} | |
Generate 5 questions that cover different aspects of the content.`, | |
}, | |
{ | |
role: "user", | |
content: JSON.stringify(transcript), | |
}, | |
], | |
response_format: { type: "json_object" }, | |
}); | |
const content = response.choices[0].message.content; | |
if (!content) { | |
throw new Error( | |
"Failed to generate quiz questions: Empty response from OpenAI", | |
); | |
} | |
return JSON.parse(content); | |
} catch (error) { | |
console.error("Quiz generation error:", error); | |
throw new Error( | |
`Failed to generate quiz: ${error instanceof Error ? error.message : "Unknown error"}`, | |
); | |
} | |
} | |
export async function analyzeFinancialStatement(content: string) { | |
try { | |
const response = await openai.chat.completions.create({ | |
model: process.env.AZURE_OPENAI_DEPLOYMENT || CHAT_MODEL, | |
messages: [ | |
{ | |
role: "system", | |
content: `You are a financial expert. Analyze this financial statement and extract key financial data. | |
Format the response as a JSON object with these fields: | |
{ | |
"summary": "Brief, clear overview of financial status", | |
"insights": ["Key insight 1", "Key insight 2", ...], | |
"categories": { | |
"cash": number, | |
"stocks": number, | |
"bonds": number, | |
"other": number | |
}, | |
"monthlyTotal": number, | |
"portfolio": { | |
"totalValue": number, | |
"returns": { | |
"monthly": number, | |
"quarterly": number, | |
"yearly": number | |
}, | |
"assetAllocation": { | |
"stocks": number, | |
"bonds": number, | |
"cash": number, | |
"other": number | |
}, | |
"topHoldings": [ | |
{ | |
"name": string, | |
"value": number, | |
"percentage": number, | |
"type": string | |
} | |
], | |
"riskMetrics": { | |
"volatility": number, | |
"sharpeRatio": number, | |
"beta": number | |
} | |
} | |
} | |
Important: | |
1. All number values must be provided and not null | |
2. Use 0 as default if a value cannot be determined | |
3. Returns should be decimal percentages (e.g., 0.05 for 5%) | |
4. Ensure all required fields are present with valid values`, | |
}, | |
{ | |
role: "user", | |
content: content, | |
}, | |
], | |
response_format: { type: "json_object" }, | |
}); | |
const result = response.choices[0].message.content; | |
if (!result) { | |
throw new Error( | |
"Failed to analyze statement: Empty response from OpenAI", | |
); | |
} | |
const parsed = JSON.parse(result); | |
// Add default holdings if not present | |
if (!parsed.portfolio.topHoldings) { | |
parsed.portfolio.topHoldings = []; | |
} | |
// Process holdings and get tickers | |
const processedHoldings = await Promise.all( | |
parsed.portfolio.topHoldings.map(async (holding: any) => { | |
const ticker = await getStockTickerFromName(holding.name); | |
return { | |
...holding, | |
symbol: ticker || undefined, | |
}; | |
}), | |
); | |
return { | |
...parsed, | |
portfolio: { | |
...parsed.portfolio, | |
topHoldings: processedHoldings, | |
}, | |
}; | |
} catch (error) { | |
console.error("Financial analysis error:", error); | |
// Return a safe default structure | |
return { | |
summary: "Unable to analyze statement", | |
insights: ["Analysis failed, please try again"], | |
categories: { | |
cash: 0, | |
stocks: 0, | |
bonds: 0, | |
other: 0, | |
}, | |
monthlyTotal: 0, | |
portfolio: { | |
totalValue: 0, | |
returns: { | |
monthly: 0, | |
quarterly: 0, | |
yearly: 0, | |
}, | |
assetAllocation: { | |
stocks: 0, | |
bonds: 0, | |
cash: 0, | |
other: 0, | |
}, | |
topHoldings: [], | |
riskMetrics: { | |
volatility: 0, | |
sharpeRatio: 0, | |
beta: 0, | |
}, | |
}, | |
}; | |
} | |
} | |
export async function generatePortfolioRadioShow(portfolioAnalysis: any) { | |
try { | |
const response = await openai.chat.completions.create({ | |
model: process.env.AZURE_OPENAI_DEPLOYMENT || CHAT_MODEL, | |
messages: [ | |
{ | |
role: "system", | |
content: `Create an engaging podcast discussion about this portfolio between two hosts: Ash (the enthusiastic host) and Sage (the expert analyst). | |
Format as JSON with this structure: | |
{ | |
"title": "Portfolio Analysis Podcast", | |
"summary": "Brief overview of what will be discussed", | |
"sections": [ | |
{ | |
"title": "Portfolio Overview", | |
"metrics": { | |
"totalValue": number, | |
"monthlyReturn": number, | |
"yearlyReturn": number | |
} | |
}, | |
{ | |
"title": "Market Context", | |
"marketConditions": { | |
"trend": "bullish|bearish|neutral", | |
"keyFactors": string[], | |
"potentialImpact": string | |
} | |
}, | |
{ | |
"title": "Risk Analysis", | |
"metrics": { | |
"volatility": number, | |
"sharpeRatio": number, | |
"beta": number | |
} | |
} | |
], | |
"transcript": { | |
"segments": [ | |
{ | |
"speaker": "Ash|Sage", | |
"text": string, | |
"timestamp": number, | |
"section": string | |
} | |
] | |
}, | |
"showMetadata": { | |
"duration": number, | |
"marketMood": "bullish|bearish|neutral", | |
"keyPoints": string[], | |
"portfolioHighlights": { | |
"strongestCategory": string, | |
"improvement": string, | |
"mainRisk": string | |
} | |
} | |
} | |
Make it engaging and conversational: | |
1. Ash should be enthusiastic and ask insightful questions | |
2. Sage should provide expert analysis and clear explanations | |
3. Include emotional markers like [excited], [thoughtful], etc. | |
4. Keep total duration around 3-5 minutes | |
5. Naturally discuss key metrics and insights | |
6. Include specific market context and its impact on the portfolio | |
7. Break down complex metrics into understandable terms`, | |
}, | |
{ | |
role: "user", | |
content: JSON.stringify(portfolioAnalysis), | |
}, | |
], | |
response_format: { type: "json_object" }, | |
}); | |
const content = response.choices[0].message.content; | |
if (!content) { | |
throw new Error( | |
"Failed to generate radio show script: Empty response from OpenAI", | |
); | |
} | |
return JSON.parse(content); | |
} catch (error) { | |
handleOpenAIError(error); | |
} | |
} | |
async function getStockTickerFromName( | |
companyName: string, | |
): Promise<string | null> { | |
try { | |
// Clean the company name | |
const cleanedName = companyName | |
.replace(/\bINC\b|\bCORP\b|\bLTD\b|\bPLC\b|\bETF\b/gi, "") | |
.replace(/[^a-zA-Z0-9\s]/g, "") | |
.trim(); | |
// Try to fetch symbol from Alpha Vantage | |
const response = await axios.get( | |
`https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=${encodeURIComponent(cleanedName)}&apikey=${process.env.ALPHA_VANTAGE_API_KEY}`, | |
); | |
if (response.data.bestMatches && response.data.bestMatches.length > 0) { | |
return response.data.bestMatches[0]["1. symbol"]; | |
} | |
return null; | |
} catch (error) { | |
console.error("Error finding ticker:", error); | |
return null; | |
} | |
} | |
export default openai; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment