A comprehensive look at the engineering challenges and solutions behind combining async web crawling, document parsing, and LLM integration in a Streamlit application.
Streamlit 1.51
Crawl4AI 0.7.6
youtube-transcript-api 1.2.3
Google Gemini 0.8.5
PyPDF2, python-docx
Playwright 1.55.0
Crawl4AI is built on async/await patterns for efficient I/O operations. However, Streamlit runs in a synchronous context that may already have an event loop running. Calling asyncio.run() inside an existing loop raises:
RuntimeError: This event loop is already runningdef run(url: str) -> str:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
# Already in an async context - delegate to a separate thread
q: queue.Queue[tuple[bool, object]] = queue.Queue()
def worker():
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
try:
res = new_loop.run_until_complete(scrape(url))
q.put((True, res))
except Exception as e:
q.put((False, e))
finally:
new_loop.close()
t = threading.Thread(target=worker, daemon=True)
t.start()
ok, val = q.get() # Block until result ready
if ok:
return val
raise val
else:
# No existing loop - safe to use asyncio.run()
return asyncio.run(scrape(url))Each async operation gets its own event loop in a dedicated thread
Thread-safe communication without shared mutable state
Auto-cleanup on application exit
Preserves exception type and traceback
Windows requires special handling for async subprocess operations:
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())The ProactorEventLoop is required on Windows for proper subprocess and pipe handling that Crawl4AI uses internally.
Different source types require different extraction strategies:
Need headless browser rendering for JavaScript-heavy sites
Standard scraping fails—need transcript API
Binary format requiring specialized parsing
XML-based format with paragraph structure
async def scrape(url: str) -> str:
# Route based on URL pattern
if _is_youtube_url(url):
vid = _extract_youtube_video_id(url)
if vid:
try:
return _fetch_youtube_transcript_text(vid)
except Exception:
pass # Fallback to web crawling
# Default: headless browser crawl
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(url)
return result.markdown or ""LLM APIs can fail for various reasons. A single API call failure shouldn't break the entire workflow.
def summarize_with_gemini(text: str, prompt: str | None = None) -> str:
genai.configure(api_key=api_key)
# Ordered by preference: latest → stable fallbacks
choices = [
"gemini-2.5-flash",
"gemini-2.0-flash",
"gemini-2.5-flash-lite",
"gemini-1.5-flash",
"gemini-1.5-pro",
]
last_err: Exception | None = None
for name in choices:
try:
model = genai.GenerativeModel(name)
resp = model.generate_content(full_prompt)
# Handle different response structures
if hasattr(resp, "text") and resp.text:
return resp.text
...
except Exception as e:
last_err = e
continue # Try next model
if last_err:
raise last_err
return ""URLs contain characters that are invalid in filenames across different operating systems. Additionally, different URLs might generate the same filename, causing data overwrites.
host + pathInvalid → _SHA1[:8]Max 64 charsEnterprise users need Word documents, but LLM output is Markdown. The conversion must preserve all formatting elements.
Heading hierarchy
Bold & Italic
`code`Inline code
Lists
Code blocks
def _add_inline_markdown(paragraph, text: str):
"""Process inline markdown: **bold**, *italic*, `code`"""
pattern = re.compile(r"(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)")
pos = 0
for m in pattern.finditer(text):
# Add plain text before match
if m.start() > pos:
paragraph.add_run(text[pos:m.start()])
token = m.group(0)
if token.startswith("**"):
r = paragraph.add_run(token[2:-2])
r.bold = True
elif token.startswith("*"):
r = paragraph.add_run(token[1:-1])
r.italic = True
elif token.startswith("`"):
r = paragraph.add_run(token[1:-1])
r.font.name = "Courier New"
pos = m.end()
# Add remaining text
if pos < len(text):
paragraph.add_run(text[pos:])Multiple users share the same application instance. Each user's data must be isolated, persisted across sessions, and efficiently queryable.
threading.Thread + queue.QueueBuilding a production-ready web scraping and LLM integration system involves solving numerous edge cases and platform-specific issues. The key patterns demonstrated here—thread-based async isolation, cascading fallbacks, and defensive data handling—are applicable to any Python application integrating external APIs with complex I/O patterns.
The combination of Streamlit for rapid prototyping, Crawl4AI for robust web scraping, and Gemini for intelligent summarization creates a powerful foundation that can be extended to various domains beyond financial analysis.
Built by Flairminds — Turning complex data pipelines into simple workflows.