Spaces:
Runtime error
Runtime error
Upload 3 files
Browse filesAdd 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.
- app.py +190 -91
- requirements.txt +4 -1
- 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 |
-
#
|
| 29 |
-
|
| 30 |
-
#
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
try:
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 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 |
-
|
| 88 |
-
model=model,
|
| 89 |
-
messages=messages,
|
| 90 |
-
temperature=temperature,
|
| 91 |
-
max_tokens=max_tokens,
|
| 92 |
-
)
|
| 93 |
except Exception:
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
)
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
else:
|
| 103 |
-
#
|
| 104 |
-
openai.api_key = api_key
|
| 105 |
try:
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|