File size: 13,708 Bytes
d356ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import logging
from typing import Annotated, Any, Dict, TypedDict
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain.tools import Tool
from langchain_core.messages import HumanMessage, AIMessage
from state import GraphState
from youtube_transcript_api import YouTubeTranscriptApi
import json
import os
from dotenv import load_dotenv

# Set up logging with more detailed format
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Load environment variables first
load_dotenv(verbose=True)
logger.debug("Current working directory: %s", os.getcwd())
logger.debug("Looking for .env file in: %s", os.path.abspath('.'))

# Check for OpenAI API key
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    logger.error("OPENAI_API_KEY not found in environment variables")
    logger.debug("Available environment variables: %s", list(os.environ.keys()))
    raise ValueError("OPENAI_API_KEY not found. Please set it in your .env file")
else:
    logger.debug("OPENAI_API_KEY found with length: %d", len(api_key))

def get_transcript_node(state: GraphState) -> GraphState:
    """Fetch transcript from YouTube video."""
    logger.info("Starting transcript fetch...")
    try:
        # Extract video ID from URL
        if "youtu.be" in state["video_url"]:
            video_id = state["video_url"].split("/")[-1]
        else:
            video_id = state["video_url"].split("v=")[-1].split("&")[0]
        logger.info(f"Extracted video ID: {video_id}")
        
        # Get transcript
        transcript = YouTubeTranscriptApi.get_transcript(video_id)
        logger.info("Successfully fetched transcript")
        
        # Combine transcript segments
        full_transcript = " ".join([segment["text"] for segment in transcript])
        logger.info(f"Combined transcript length: {len(full_transcript)} characters")
        
        # Update state
        new_state = state.copy()
        new_state["transcript"] = full_transcript
        return new_state
    except Exception as e:
        logger.error(f"Error fetching transcript: {str(e)}", exc_info=True)
        new_state = state.copy()
        new_state["error"] = f"Error fetching transcript: {str(e)}"
        return new_state

def enhance_text_node(state: GraphState) -> GraphState:
    """Enhance content using RAG."""
    logger.info("Starting content enhancement...")
    try:
        if "error" in state and state["error"]:
            logger.error(f"Skipping enhancement due to previous error: {state['error']}")
            return state
            
        if "transcript" not in state or not state["transcript"]:
            error_msg = "No transcript available for enhancement"
            logger.error(error_msg)
            new_state = state.copy()
            new_state["error"] = error_msg
            return new_state
            
        # Initialize LLM
        llm = ChatOpenAI(temperature=0.7)
        logger.info("Initialized LLM")
        
        # Create vector store
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
        texts = text_splitter.split_text(state["transcript"])
        logger.info(f"Split transcript into {len(texts)} chunks")
        
        embeddings = OpenAIEmbeddings()
        vector_store = Chroma.from_texts(texts, embeddings)
        logger.info("Created vector store")
        
        # Create RAG chain
        prompt = ChatPromptTemplate.from_messages([
            ("system", """You are an expert content enhancer. Your task is to:
            1. Analyze the provided transcript
            2. Identify key points and insights
            3. Enhance the content while maintaining accuracy
            4. Add relevant context and examples
            5. Ensure the output is professional and engaging
            6. Keep the enhanced content within 2000 characters
            
            Use the following context to enhance the content:
            {context}
            
            Original transcript:
            {transcript}"""),
            ("human", "Please enhance this content for a professional audience.")
        ])
        
        chain = prompt | llm | StrOutputParser()
        logger.info("Created RAG chain")
        
        # Get relevant context
        retriever = vector_store.as_retriever(search_kwargs={"k": 3})
        context = retriever.get_relevant_documents(state["transcript"])
        context_text = "\n".join([doc.page_content for doc in context])
        logger.info(f"Retrieved {len(context)} relevant documents")
        
        # Generate enhanced content
        enhanced_text = chain.invoke({
            "context": context_text,
            "transcript": state["transcript"]
        })
        logger.info(f"Generated enhanced content of length: {len(enhanced_text)}")
        
        # Update state
        new_state = state.copy()
        new_state["enhanced_text"] = enhanced_text
        return new_state
    except Exception as e:
        logger.error(f"Error enhancing content: {str(e)}", exc_info=True)
        new_state = state.copy()
        new_state["error"] = f"Error enhancing content: {str(e)}"
        return new_state

def format_content_node(state: GraphState) -> GraphState:
    """Format content for LinkedIn."""
    logger.info("Starting content formatting...")
    try:
        if "error" in state and state["error"]:
            logger.error(f"Skipping formatting due to previous error: {state['error']}")
            return state
            
        if "enhanced_text" not in state or not state["enhanced_text"]:
            error_msg = "No enhanced content available for formatting"
            logger.error(error_msg)
            new_state = state.copy()
            new_state["error"] = error_msg
            return new_state
            
        # Initialize LLM
        llm = ChatOpenAI(temperature=0.7)
        logger.info("Initialized LLM")
        
        # Create formatting chain
        prompt = ChatPromptTemplate.from_messages([
            ("system", """You are an expert LinkedIn content formatter. Your task is to:
            1. Transform the enhanced text into a professional LinkedIn post
            2. Use appropriate LinkedIn formatting (emojis, line breaks, etc.)
            3. Include relevant hashtags
            4. Add a compelling hook
            5. End with a call to action
            6. Maximum length should be around 1300 characters
            
            Enhanced text:
            {enhanced_text}"""),
            ("human", "Please format this content for LinkedIn.")
        ])
        
        chain = prompt | llm | StrOutputParser()
        logger.info("Created formatting chain")
        
        # Format content
        linkedin_formatted = chain.invoke({"enhanced_text": state["enhanced_text"]})
        logger.info(f"Generated LinkedIn post of length: {len(linkedin_formatted)}")
        
        # Update state
        new_state = state.copy()
        new_state["linkedin_formatted"] = linkedin_formatted
        logger.info("Updated state with LinkedIn formatted content")
        
        return new_state
    except Exception as e:
        logger.error(f"Error formatting content: {str(e)}", exc_info=True)
        new_state = state.copy()
        new_state["error"] = f"Error formatting content: {str(e)}"
        return new_state

def verify_content_node(state: GraphState) -> GraphState:
    """Verify the enhanced content against the original transcript."""
    logger.info("Starting content verification...")
    try:
        if "error" in state and state["error"]:
            logger.error(f"Skipping verification due to previous error: {state['error']}")
            return state
            
        if "transcript" not in state or not state["transcript"] or \
           "enhanced_text" not in state or not state["enhanced_text"]:
            error_msg = "Missing transcript or enhanced content for verification"
            logger.error(error_msg)
            new_state = state.copy()
            new_state["error"] = error_msg
            return new_state
            
        # Initialize LLM
        llm = ChatOpenAI(temperature=0)
        logger.info("Initialized LLM")
        
        # Create verification chain
        prompt = ChatPromptTemplate.from_messages([
            ("system", """You are a content verification expert. Compare the enhanced content with the original transcript.
Check for:
1. Factual accuracy
2. Key message preservation
3. No significant deviations from original meaning
4. No added misinformation

Return a JSON with:
{{
    "verified": boolean,
    "details": {{
        "accuracy_score": float (0-1),
        "key_points_preserved": boolean,
        "deviations": [list of significant deviations],
        "recommendations": [list of improvement suggestions]
    }}
}}

Original Transcript:
{transcript}

Enhanced Content:
{enhanced_text}"""),
            ("human", "Please verify this content.")
        ])
        
        chain = prompt | llm | StrOutputParser()
        logger.info("Created verification chain")
        
        # Get verification result
        verification_result = json.loads(chain.invoke({
            "transcript": state["transcript"],
            "enhanced_text": state["enhanced_text"]
        }))
        logger.info("Successfully parsed verification result")
        
        # Update state with verification results
        new_state = state.copy()
        new_state["verification_status"] = verification_result["verified"]
        new_state["verification_details"] = verification_result["details"]
        logger.info(f"Verification status: {verification_result['verified']}")
        
        return new_state
    except Exception as e:
        logger.error(f"Error verifying content: {str(e)}", exc_info=True)
        new_state = state.copy()
        new_state["error"] = f"Error verifying content: {str(e)}"
        return new_state

def fetch_transcript_node(state: GraphState) -> GraphState:
    """Fetch transcript from YouTube video."""
    logger.info("Starting transcript fetch...")
    try:
        # Extract video ID from URL
        video_id = state["video_url"].split("v=")[-1]
        logger.info(f"Extracted video ID: {video_id}")
        
        # Get transcript
        transcript = YouTubeTranscriptApi.get_transcript(video_id)
        logger.info("Successfully fetched transcript")
        
        # Combine transcript segments
        full_transcript = " ".join([segment["text"] for segment in transcript])
        logger.info(f"Combined transcript length: {len(full_transcript)} characters")
        
        return {**state, "transcript": full_transcript}
    except Exception as e:
        logger.error(f"Error fetching transcript: {str(e)}")
        return {**state, "error": f"Error fetching transcript: {str(e)}"}

def enhance_content_node(state: GraphState) -> GraphState:
    """Enhance content using RAG."""
    logger.info("Starting content enhancement...")
    try:
        # Initialize LLM
        llm = ChatOpenAI(temperature=0.7)
        logger.info("Initialized LLM")
        
        # Create vector store
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
        texts = text_splitter.split_text(state["transcript"])
        logger.info(f"Split transcript into {len(texts)} chunks")
        
        embeddings = OpenAIEmbeddings()
        vector_store = Chroma.from_texts(texts, embeddings)
        logger.info("Created vector store")
        
        # Create RAG chain
        prompt = ChatPromptTemplate.from_messages([
            ("system", """You are an expert content enhancer. Your task is to:
            1. Analyze the provided transcript
            2. Identify key points and insights
            3. Enhance the content while maintaining accuracy
            4. Add relevant context and examples
            5. Ensure the output is professional and engaging
            6. Keep the enhanced content within 2000 characters
            
            Use the following context to enhance the content:
            {context}
            
            Original transcript:
            {transcript}"""),
            ("human", "Please enhance this content for a professional audience.")
        ])
        
        chain = prompt | llm | StrOutputParser()
        logger.info("Created RAG chain")
        
        # Get relevant context
        retriever = vector_store.as_retriever(search_kwargs={"k": 3})
        context = retriever.get_relevant_documents(state["transcript"])
        context_text = "\n".join([doc.page_content for doc in context])
        logger.info(f"Retrieved {len(context)} relevant documents")
        
        # Generate enhanced content
        enhanced_text = chain.invoke({
            "context": context_text,
            "transcript": state["transcript"]
        })
        logger.info(f"Generated enhanced content of length: {len(enhanced_text)}")
        
        return {
            **state,
            "enhanced_text": enhanced_text,
            "vector_store": vector_store,
            "context": context_text
        }
    except Exception as e:
        logger.error(f"Error enhancing content: {str(e)}")
        return {**state, "error": f"Error enhancing content: {str(e)}"}