Prof-Reza commited on
Commit
617daa2
·
verified ·
1 Parent(s): a977462

Upload 3 files

Browse files

Add webpage and YouTube content extraction and summarization.

This commit enhances the course creator agent to read and analyze external links directly in chat. It updates searcher.py to include extract_web_content (via Tavily extract API) and get_youtube_transcript (using youtube-transcript-api), while preserving run_web_search. In app.py, the assistant detects URLs in user messages, fetches page content or video transcripts, and summarises them with OpenAI before replying. Requirements are updated to include youtube-transcript-api. The assistant can now open webpages and YouTube videos, summarise the content, and use it for brainstorming.

Files changed (3) hide show
  1. app.py +190 -91
  2. requirements.txt +4 -1
  3. searcher.py +64 -0
app.py CHANGED
@@ -4,7 +4,7 @@ import openai
4
 
5
  from planner import plan_course
6
  from generators import generate_course_zip
7
- from searcher import run_web_search
8
 
9
  # System prompt guiding the assistant's behaviour during brainstorming
10
  SYSTEM_PROMPT = (
@@ -25,106 +25,205 @@ def chat(user_message, chat_history, chat_pairs, sources, plan):
25
  chat_history.append({"role": "user", "content": user_message})
26
  # Build messages including system prompt for API call
27
  messages = [{"role": "system", "content": SYSTEM_PROMPT}] + chat_history
28
- # Determine if the user is requesting a web search. If so, perform the search instead
29
- # of calling the language model. This allows the assistant to fetch resources when
30
- # the user asks the agent to "search" or "search the internet".
31
- search_triggers = ["search", "internet search", "web search"]
32
- lower_msg = user_message.lower()
33
- if any(trig in lower_msg for trig in search_triggers):
 
 
 
34
  try:
35
- # Perform web search using the entire user message as the query
36
- results = run_web_search(user_message, num_results=5, domain_filter="")
37
- # Normalize results:
38
- # Tavily may return a dictionary with a "results" key containing
39
- # the list of search results. If so, extract that list. If it's a
40
- # list already, use it directly. Otherwise, default to an empty list.
41
- if isinstance(results, dict):
42
- normalized_results = results.get("results", [])
43
- elif isinstance(results, list):
44
- normalized_results = results
45
- else:
46
- normalized_results = []
47
- # Ensure the sources list is initialised
48
- if sources is None:
49
- sources = []
50
- sources.extend(normalized_results)
51
- # Summarise results into a simple string with title and URL
52
- summary_lines = []
53
- for r in normalized_results:
54
- # Defensive: ensure r is a dict
55
- if isinstance(r, dict):
56
- title = r.get("title", "")
57
- url = r.get("url", "")
58
- if title or url:
59
- summary_lines.append(f"{title} - {url}")
60
- if summary_lines:
61
- assistant_reply = "Here are some resources I found:\n" + "\n".join(summary_lines)
62
- else:
63
- assistant_reply = "I couldn't find any results for that query."
64
- except Exception as e:
65
- assistant_reply = (
66
- "An error occurred during web search. Please ensure your search API key is configured.\n"
67
- f"(Error: {e})"
68
- )
69
- else:
70
- # Call OpenAI's ChatCompletion to get assistant's reply
71
- try:
72
- # Use a widely supported default model; older OpenAI SDKs (pinned below v1)
73
- # do not recognise newer model names like gpt-5. Default to gpt-3.5-turbo
74
- # but allow overriding via the OPENAI_MODEL env variable.
75
- model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
76
- temperature = float(os.getenv("TEMPERATURE", "0.7"))
77
- max_tokens = int(os.getenv("MAX_OUTPUT_TOKENS", "1024"))
78
- # Support alternative secret name COURSECREATOR_API_KEY as a fallback for the OpenAI API key
79
- api_key = os.getenv("OPENAI_API_KEY") or os.getenv("COURSECREATOR_API_KEY")
80
- if not api_key:
81
- raise ValueError("OPENAI_API_KEY or COURSECREATOR_API_KEY is not set")
82
- # Prefer the new OpenAI SDK (>=1.0) if available
83
- if hasattr(openai, "OpenAI"):
84
- client = openai.OpenAI(api_key=api_key)
85
- # Try sending max_tokens; if unsupported, retry with max_completion_tokens
86
  try:
87
- response = client.chat.completions.create(
88
- model=model,
89
- messages=messages,
90
- temperature=temperature,
91
- max_tokens=max_tokens,
92
- )
93
  except Exception:
94
- # Some newer models (e.g. o1 series) do not support max_tokens
95
- response = client.chat.completions.create(
96
- model=model,
97
- messages=messages,
98
- temperature=temperature,
99
- max_completion_tokens=max_tokens,
100
- )
101
- assistant_reply = response.choices[0].message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  else:
103
- # Legacy OpenAI SDK (<1.0)
104
- openai.api_key = api_key
105
  try:
106
- response = openai.ChatCompletion.create(
107
- model=model,
108
- messages=messages,
109
- temperature=temperature,
110
- max_tokens=max_tokens,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  )
112
- except Exception:
113
- # Fallback for models that require max_completion_tokens
114
- response = openai.ChatCompletion.create(
115
- model=model,
116
- messages=messages,
117
- temperature=temperature,
118
- max_completion_tokens=max_tokens,
119
- )
120
- assistant_reply = response["choices"][0]["message"]["content"]
121
  except Exception as e:
122
- # When the API call fails (e.g. missing API key), return an error message
123
  assistant_reply = (
124
- "An error occurred while processing your message. "
125
- "Please ensure your OpenAI API key is configured in the Space secrets.\n"
126
  f"(Error: {e})"
127
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  # Append assistant reply to conversation history
129
  chat_history.append({"role": "assistant", "content": assistant_reply})
130
  # Append pair to display history for any other uses (kept for compatibility)
 
4
 
5
  from planner import plan_course
6
  from generators import generate_course_zip
7
+ from searcher import run_web_search, extract_web_content, get_youtube_transcript
8
 
9
  # System prompt guiding the assistant's behaviour during brainstorming
10
  SYSTEM_PROMPT = (
 
25
  chat_history.append({"role": "user", "content": user_message})
26
  # Build messages including system prompt for API call
27
  messages = [{"role": "system", "content": SYSTEM_PROMPT}] + chat_history
28
+ # Check if the user message contains a URL to open and read.
29
+ url = None
30
+ # Simple heuristic: look for http/https links in the message
31
+ for part in user_message.split():
32
+ if part.startswith("http://") or part.startswith("https://"):
33
+ url = part
34
+ break
35
+ if url:
36
+ # User is asking to open/read a specific page or YouTube video
37
  try:
38
+ page_content = ""
39
+ # Special handling for YouTube links: attempt to fetch transcript
40
+ if "youtube.com" in url or "youtu.be" in url:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  try:
42
+ transcript_text = get_youtube_transcript(url)
 
 
 
 
 
43
  except Exception:
44
+ transcript_text = ""
45
+ page_content = transcript_text or ""
46
+ # For non-YouTube links or fallback if transcript empty, use Tavily extract
47
+ if not page_content:
48
+ extract_response = extract_web_content(url)
49
+ if isinstance(extract_response, dict):
50
+ if extract_response.get("content"):
51
+ page_content = extract_response.get("content", "")
52
+ elif extract_response.get("text"):
53
+ page_content = extract_response.get("text", "")
54
+ elif extract_response.get("article"):
55
+ page_content = extract_response.get("article", "")
56
+ elif extract_response.get("results"):
57
+ results_list = extract_response.get("results", [])
58
+ if isinstance(results_list, list):
59
+ page_content = "\n".join([
60
+ item.get("content", item.get("title", ""))
61
+ for item in results_list
62
+ if isinstance(item, dict)
63
+ ])
64
+ if not page_content:
65
+ assistant_reply = "I couldn't extract content from that page."
66
  else:
67
+ # Summarise the extracted content using OpenAI
 
68
  try:
69
+ model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
70
+ temperature = float(os.getenv("TEMPERATURE", "0.7"))
71
+ max_tokens = int(os.getenv("MAX_OUTPUT_TOKENS", "512"))
72
+ api_key = os.getenv("OPENAI_API_KEY") or os.getenv("COURSECREATOR_API_KEY")
73
+ if not api_key:
74
+ raise ValueError("OPENAI_API_KEY or COURSECREATOR_API_KEY is not set")
75
+ summary_system = "You are a helpful assistant. Summarize the given content in a concise and clear way."
76
+ # Truncate content to avoid exceeding token limits
77
+ truncated_content = page_content[:8000]
78
+ summary_messages = [
79
+ {"role": "system", "content": summary_system},
80
+ {"role": "user", "content": truncated_content},
81
+ ]
82
+ if hasattr(openai, "OpenAI"):
83
+ client = openai.OpenAI(api_key=api_key)
84
+ try:
85
+ resp = client.chat.completions.create(
86
+ model=model,
87
+ messages=summary_messages,
88
+ temperature=temperature,
89
+ max_tokens=max_tokens,
90
+ )
91
+ except Exception:
92
+ resp = client.chat.completions.create(
93
+ model=model,
94
+ messages=summary_messages,
95
+ temperature=temperature,
96
+ max_completion_tokens=max_tokens,
97
+ )
98
+ assistant_reply = resp.choices[0].message.content
99
+ else:
100
+ openai.api_key = api_key
101
+ try:
102
+ resp = openai.ChatCompletion.create(
103
+ model=model,
104
+ messages=summary_messages,
105
+ temperature=temperature,
106
+ max_tokens=max_tokens,
107
+ )
108
+ except Exception:
109
+ resp = openai.ChatCompletion.create(
110
+ model=model,
111
+ messages=summary_messages,
112
+ temperature=temperature,
113
+ max_completion_tokens=max_tokens,
114
+ )
115
+ assistant_reply = resp["choices"][0]["message"]["content"]
116
+ except Exception as e:
117
+ assistant_reply = (
118
+ "An error occurred while summarizing the page content. Please ensure your OpenAI API key is configured.\n"
119
+ f"(Error: {e})"
120
  )
 
 
 
 
 
 
 
 
 
121
  except Exception as e:
 
122
  assistant_reply = (
123
+ "An error occurred while extracting the web page. Please ensure your search API key is configured.\n"
 
124
  f"(Error: {e})"
125
  )
126
+ else:
127
+ # Determine if the user is requesting a web search. If so, perform the search instead
128
+ # of calling the language model. This allows the assistant to fetch resources when
129
+ # the user asks the agent to "search" or "search the internet".
130
+ search_triggers = ["search", "internet search", "web search"]
131
+ lower_msg = user_message.lower()
132
+ if any(trig in lower_msg for trig in search_triggers):
133
+ try:
134
+ # Perform web search using the entire user message as the query
135
+ results = run_web_search(user_message, num_results=5, domain_filter="")
136
+ # Normalize results:
137
+ # Tavily may return a dictionary with a "results" key containing
138
+ # the list of search results. If so, extract that list. If it's a
139
+ # list already, use it directly. Otherwise, default to an empty list.
140
+ if isinstance(results, dict):
141
+ normalized_results = results.get("results", [])
142
+ elif isinstance(results, list):
143
+ normalized_results = results
144
+ else:
145
+ normalized_results = []
146
+ # Ensure the sources list is initialised
147
+ if sources is None:
148
+ sources = []
149
+ sources.extend(normalized_results)
150
+ # Summarise results into a simple string with title and URL
151
+ summary_lines = []
152
+ for r in normalized_results:
153
+ # Defensive: ensure r is a dict
154
+ if isinstance(r, dict):
155
+ title = r.get("title", "")
156
+ url = r.get("url", "")
157
+ if title or url:
158
+ summary_lines.append(f"{title} - {url}")
159
+ if summary_lines:
160
+ assistant_reply = "Here are some resources I found:\n" + "\n".join(summary_lines)
161
+ else:
162
+ assistant_reply = "I couldn't find any results for that query."
163
+ except Exception as e:
164
+ assistant_reply = (
165
+ "An error occurred during web search. Please ensure your search API key is configured.\n"
166
+ f"(Error: {e})"
167
+ )
168
+ else:
169
+ # Call OpenAI's ChatCompletion to get assistant's reply
170
+ try:
171
+ # Use a widely supported default model; older OpenAI SDKs (pinned below v1)
172
+ # do not recognise newer model names like gpt-5. Default to gpt-3.5-turbo
173
+ # but allow overriding via the OPENAI_MODEL env variable.
174
+ model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
175
+ temperature = float(os.getenv("TEMPERATURE", "0.7"))
176
+ max_tokens = int(os.getenv("MAX_OUTPUT_TOKENS", "1024"))
177
+ # Support alternative secret name COURSECREATOR_API_KEY as a fallback for the OpenAI API key
178
+ api_key = os.getenv("OPENAI_API_KEY") or os.getenv("COURSECREATOR_API_KEY")
179
+ if not api_key:
180
+ raise ValueError("OPENAI_API_KEY or COURSECREATOR_API_KEY is not set")
181
+ # Prefer the new OpenAI SDK (>=1.0) if available
182
+ if hasattr(openai, "OpenAI"):
183
+ client = openai.OpenAI(api_key=api_key)
184
+ # Try sending max_tokens; if unsupported, retry with max_completion_tokens
185
+ try:
186
+ response = client.chat.completions.create(
187
+ model=model,
188
+ messages=messages,
189
+ temperature=temperature,
190
+ max_tokens=max_tokens,
191
+ )
192
+ except Exception:
193
+ # Some newer models (e.g. o1 series) do not support max_tokens
194
+ response = client.chat.completions.create(
195
+ model=model,
196
+ messages=messages,
197
+ temperature=temperature,
198
+ max_completion_tokens=max_tokens,
199
+ )
200
+ assistant_reply = response.choices[0].message.content
201
+ else:
202
+ # Legacy OpenAI SDK (<1.0)
203
+ openai.api_key = api_key
204
+ try:
205
+ response = openai.ChatCompletion.create(
206
+ model=model,
207
+ messages=messages,
208
+ temperature=temperature,
209
+ max_tokens=max_tokens,
210
+ )
211
+ except Exception:
212
+ # Fallback for models that require max_completion_tokens
213
+ response = openai.ChatCompletion.create(
214
+ model=model,
215
+ messages=messages,
216
+ temperature=temperature,
217
+ max_completion_tokens=max_tokens,
218
+ )
219
+ assistant_reply = response["choices"][0]["message"]["content"]
220
+ except Exception as e:
221
+ # When the API call fails (e.g. missing API key), return an error message
222
+ assistant_reply = (
223
+ "An error occurred while processing your message. "
224
+ "Please ensure your OpenAI API key is configured in the Space secrets.\n"
225
+ f"(Error: {e})"
226
+ )
227
  # Append assistant reply to conversation history
228
  chat_history.append({"role": "assistant", "content": assistant_reply})
229
  # Append pair to display history for any other uses (kept for compatibility)
requirements.txt CHANGED
@@ -2,4 +2,7 @@ gradio>=3.0.0
2
  openai<1.0.0
3
  tavily-python>=0.3.0
4
  pydantic>=1.10.0
5
- python-dotenv>=1.0.0
 
 
 
 
2
  openai<1.0.0
3
  tavily-python>=0.3.0
4
  pydantic>=1.10.0
5
+ python-dotenv>=1.0.0
6
+
7
+ # Allow the agent to fetch YouTube video transcripts for summarization
8
+ youtube-transcript-api>=1.0.0
searcher.py CHANGED
@@ -17,3 +17,67 @@ def run_web_search(query, num_results=5, domain_filter=""):
17
  params["search_kwargs"] = {"site": domain_filter}
18
  results = client.search(query, **params)
19
  return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  params["search_kwargs"] = {"site": domain_filter}
18
  results = client.search(query, **params)
19
  return results
20
+
21
+ # New function to extract content from a given URL using Tavily Extract API.
22
+ def extract_web_content(url):
23
+ """Extract the main content of a web page via Tavily Extract.
24
+
25
+ Args:
26
+ url (str): The URL of the page to extract.
27
+
28
+ Returns:
29
+ dict: The Tavily extract response containing page content and metadata.
30
+
31
+ Raises:
32
+ ImportError: If the tavily-python package is missing.
33
+ ValueError: If the TAVILY_API_KEY environment variable is not set.
34
+ """
35
+ try:
36
+ from tavily import TavilyClient
37
+ except ImportError:
38
+ raise ImportError("Please install tavily-python")
39
+ api_key = os.getenv("TAVILY_API_KEY")
40
+ if not api_key:
41
+ raise ValueError("TAVILY_API_KEY environment variable is required")
42
+ client = TavilyClient(api_key=api_key)
43
+ # Call the extract endpoint to retrieve structured content from the URL
44
+ response = client.extract(url)
45
+ return response
46
+
47
+ # New function to get a YouTube video transcript given its URL
48
+ def get_youtube_transcript(video_url):
49
+ """Fetch the transcript of a YouTube video using youtube-transcript-api.
50
+
51
+ Args:
52
+ video_url (str): The full URL to a YouTube video.
53
+
54
+ Returns:
55
+ str: The concatenated transcript text, or an empty string if none found.
56
+
57
+ Raises:
58
+ ImportError: If youtube-transcript-api is not installed.
59
+ """
60
+ # Parse the video ID from the URL
61
+ try:
62
+ from urllib.parse import urlparse, parse_qs
63
+ from youtube_transcript_api import YouTubeTranscriptApi
64
+ except ImportError:
65
+ raise ImportError("Please install youtube-transcript-api for YouTube transcript extraction")
66
+ parsed = urlparse(video_url)
67
+ video_id = None
68
+ if "youtube.com" in parsed.netloc:
69
+ # Extract v parameter
70
+ query = parse_qs(parsed.query)
71
+ video_id = query.get("v", [None])[0]
72
+ elif "youtu.be" in parsed.netloc:
73
+ # Shortened link; path contains the ID
74
+ video_id = parsed.path.strip("/")
75
+ if not video_id:
76
+ return ""
77
+ try:
78
+ transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
79
+ except Exception:
80
+ return ""
81
+ # Concatenate all transcript segments into a single string
82
+ transcript_text = " ".join(seg.get("text", "") for seg in transcript_list)
83
+ return transcript_text