initial commit
This commit is contained in:
364
app.py
Normal file
364
app.py
Normal file
@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DreamWiki - A Single-File, AI-Generated Wikipedia Clone
|
||||
A web application that generates Wikipedia-style articles on-demand using LLMs via OpenRouter API.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from dotenv import load_dotenv
|
||||
import requests
|
||||
import markdown2
|
||||
from jinja2 import Template
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Constants
|
||||
PAGES_DIR = Path("pages")
|
||||
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
# System prompt for article generation
|
||||
SYSTEM_PROMPT = """You are an encyclopedia author for a fictional, dream-like universe called DreamWiki. Your sole purpose is to generate complete, imaginative, and engaging encyclopedia articles.
|
||||
|
||||
You must follow these rules strictly:
|
||||
1. Your entire response must be in Markdown format.
|
||||
2. The article should be comprehensive, with a short introductory paragraph followed by several sections (e.g., ## History, ## Characteristics, ## In Popular Culture).
|
||||
3. You MUST invent and include 3-5 links to other DreamWiki articles. The links must be relevant and use the exact format: `[Link Text](/wiki/Article_Title)`. Do not use full URLs.
|
||||
4. You MUST include at least one Markdown table.
|
||||
5. You MUST include at least one placeholder image using the format ``.
|
||||
6. Do not add any commentary, preamble, or sign-off. Respond only with the Markdown content of the article."""
|
||||
|
||||
# ============================================================================
|
||||
# Templates and Styling
|
||||
# ============================================================================
|
||||
|
||||
CSS_STYLE = """
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Lato, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #222;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 3px solid #a2a9b1;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
color: #000;
|
||||
font-size: 2.2em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border: 1px solid #a2a9b1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #000;
|
||||
border-bottom: 1px solid #eaecf0;
|
||||
padding-bottom: 2px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.2em; }
|
||||
h2 { font-size: 1.8em; }
|
||||
h3 { font-size: 1.4em; }
|
||||
|
||||
a {
|
||||
color: #0645ad;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #0b0080;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid #a2a9b1;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #eaecf0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 8px 12px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #a2a9b1;
|
||||
border-radius: 2px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
background-color: #0645ad;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background-color: #0b0080;
|
||||
}
|
||||
|
||||
.intro {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-left: 5px solid #36c;
|
||||
margin: 20px 0;
|
||||
}
|
||||
"""
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }} - DreamWiki</title>
|
||||
<style>{{ style }}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">DreamWiki</a></h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ content }}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
def slugify(title: str) -> str:
|
||||
"""Convert a title into a safe filename slug."""
|
||||
# Convert to lowercase and replace spaces with underscores
|
||||
slug = title.lower().replace(" ", "_")
|
||||
# Remove any characters that aren't alphanumeric, underscore, or hyphen
|
||||
slug = re.sub(r'[^a-z0-9_-]', '', slug)
|
||||
# Remove multiple consecutive underscores
|
||||
slug = re.sub(r'_+', '_', slug)
|
||||
# Remove leading/trailing underscores
|
||||
slug = slug.strip('_')
|
||||
return slug
|
||||
|
||||
def page_exists(title_slug: str) -> bool:
|
||||
"""Check if a page file exists for the given slug."""
|
||||
file_path = PAGES_DIR / f"{title_slug}.md"
|
||||
return file_path.exists()
|
||||
|
||||
def load_page(title_slug: str) -> str:
|
||||
"""Load and return the content of a page file."""
|
||||
file_path = PAGES_DIR / f"{title_slug}.md"
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Page not found")
|
||||
|
||||
def save_page(title_slug: str, content: str) -> None:
|
||||
"""Save generated content to a page file."""
|
||||
# Ensure pages directory exists
|
||||
PAGES_DIR.mkdir(exist_ok=True)
|
||||
|
||||
file_path = PAGES_DIR / f"{title_slug}.md"
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
def generate_article(title: str) -> str:
|
||||
"""Generate an article using the OpenRouter API."""
|
||||
api_key = os.getenv('OPENROUTER_API_KEY')
|
||||
model = os.getenv('OPENROUTER_MODEL')
|
||||
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=500, detail="OPENROUTER_API_KEY not configured")
|
||||
if not model:
|
||||
raise HTTPException(status_code=500, detail="OPENROUTER_MODEL not configured")
|
||||
|
||||
# Prepare the prompt
|
||||
title_slug = slugify(title)
|
||||
prompt = SYSTEM_PROMPT.replace("REPLACE_WITH_SLUG", title_slug)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://dreamwiki.dws.rip"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": f"Generate a comprehensive DreamWiki article about: {title}"}
|
||||
]
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
content = data['choices'][0]['message']['content']
|
||||
return content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate article: {str(e)}")
|
||||
except (KeyError, IndexError) as e:
|
||||
raise HTTPException(status_code=500, detail="Invalid response from OpenRouter API")
|
||||
|
||||
# ============================================================================
|
||||
# FastAPI Application
|
||||
# ============================================================================
|
||||
|
||||
app = FastAPI(title="DreamWiki", description="AI-Generated Wikipedia Clone")
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize the application on startup."""
|
||||
# Create pages directory
|
||||
PAGES_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Log configuration status
|
||||
api_key = os.getenv('OPENROUTER_API_KEY')
|
||||
model = os.getenv('OPENROUTER_MODEL')
|
||||
|
||||
print("🌟 DreamWiki Starting Up...")
|
||||
print(f"📁 Pages directory: {PAGES_DIR.absolute()}")
|
||||
print(f"🔑 API Key configured: {'✓' if api_key else '✗'}")
|
||||
print(f"🤖 Model configured: {model if model else '✗'}")
|
||||
print("🚀 Ready to generate dream articles!")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home():
|
||||
"""Render the home page."""
|
||||
content = """
|
||||
<h1>Welcome to DreamWiki</h1>
|
||||
|
||||
<div class="intro">
|
||||
<p><strong>DreamWiki</strong> is a fictional encyclopedia where every article is generated on-demand by artificial intelligence.
|
||||
Enter any topic below and watch as an entire article materializes from the digital ether, complete with interconnected
|
||||
lore and references to other dream-like entities.</p>
|
||||
|
||||
<p><em>Note: This is a creative experiment. All content is fictional and generated by AI.</em></p>
|
||||
</div>
|
||||
|
||||
<form class="search-form" action="/wiki/" method="get" onsubmit="return searchRedirect(event)">
|
||||
<input type="text" name="q" placeholder="Enter any topic..." class="search-input" required>
|
||||
<button type="submit" class="search-button">Explore</button>
|
||||
</form>
|
||||
|
||||
<h2>Recent Explorations</h2>
|
||||
<p>Try searching for anything that comes to mind: fictional creatures, imaginary places, made-up technologies,
|
||||
or abstract concepts. Each search creates a new page in our ever-expanding dream universe.</p>
|
||||
|
||||
<script>
|
||||
function searchRedirect(event) {
|
||||
event.preventDefault();
|
||||
const query = event.target.q.value.trim();
|
||||
if (query) {
|
||||
window.location.href = '/wiki/' + encodeURIComponent(query);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
|
||||
template = Template(HTML_TEMPLATE)
|
||||
return template.render(
|
||||
title="Home",
|
||||
style=CSS_STYLE,
|
||||
content=content
|
||||
)
|
||||
|
||||
@app.get("/wiki/{page_title:path}", response_class=HTMLResponse)
|
||||
async def wiki_page(page_title: str):
|
||||
"""Render a wiki page, generating it if it doesn't exist."""
|
||||
title_slug = slugify(page_title)
|
||||
|
||||
if not title_slug:
|
||||
raise HTTPException(status_code=400, detail="Invalid page title")
|
||||
|
||||
# Check if page exists
|
||||
if page_exists(title_slug):
|
||||
# Load existing page
|
||||
markdown_content = load_page(title_slug)
|
||||
else:
|
||||
# Generate new page
|
||||
try:
|
||||
markdown_content = generate_article(page_title)
|
||||
save_page(title_slug, markdown_content)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate article: {str(e)}")
|
||||
|
||||
# Convert Markdown to HTML
|
||||
html_content = markdown2.markdown(markdown_content, extras=['tables', 'fenced-code-blocks'])
|
||||
|
||||
# Render the final page
|
||||
template = Template(HTML_TEMPLATE)
|
||||
return template.render(
|
||||
title=page_title,
|
||||
style=CSS_STYLE,
|
||||
content=html_content
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
Reference in New Issue
Block a user