import zipfile import io import json from typing import Dict, Any def generate_export_package( chatbot: Dict[str, Any], company: Dict[str, Any], qdrant_url: str, qdrant_key: str, ) -> bytes: """ Generate a complete export ZIP with FastAPI backend + React widget """ buffer = io.BytesIO() with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf: # ── Backend files ────────────────────────────────────────── zf.writestr("backend/requirements.txt", _requirements()) zf.writestr("backend/.env.example", _env_example(chatbot, qdrant_url, qdrant_key)) zf.writestr("backend/main.py", _main_py(chatbot)) zf.writestr("backend/rag_engine.py", _rag_engine_py()) zf.writestr("backend/Dockerfile", _dockerfile()) zf.writestr("backend/docker-compose.yml", _docker_compose(chatbot)) zf.writestr("backend/README.md", _backend_readme(chatbot)) # ── Frontend files ───────────────────────────────────────── zf.writestr("frontend/src/ChatWidget.tsx", _chat_widget_tsx(chatbot)) zf.writestr("frontend/src/useChat.ts", _use_chat_ts()) zf.writestr("frontend/src/api.ts", _api_ts()) zf.writestr("frontend/src/types.ts", _types_ts()) zf.writestr("frontend/package.json", _package_json(chatbot)) zf.writestr("frontend/tsconfig.json", _tsconfig()) zf.writestr("frontend/vite.config.ts", _vite_config()) zf.writestr("frontend/README.md", _frontend_readme(chatbot)) # ── Root ─────────────────────────────────────────────────── zf.writestr("QUICK_START.md", _quick_start(chatbot)) zf.writestr("setup.py", _setup_wizard(chatbot)) buffer.seek(0) return buffer.read() def _requirements(): return """fastapi==0.115.0 uvicorn[standard]==0.30.6 python-dotenv==1.0.1 pydantic==2.8.2 qdrant-client==1.11.1 openai==1.51.0 anthropic==0.34.2 google-generativeai==0.8.1 httpx==0.27.2 langdetect==1.0.9 """ # BUG-14 FIX: Helper to safely escape strings for use in generated Python code def _escape_for_python(value: str) -> str: """Escape a string so it can be safely embedded in generated Python source code. Uses json.dumps which properly escapes quotes, backslashes, and special chars.""" return json.dumps(value) def _env_example(chatbot: Dict, qdrant_url: str, qdrant_key: str): name = chatbot.get("name", "My Chatbot").upper().replace(" ", "_") return f"""# {name} - Environment Configuration # Copy to .env and fill in your values # LLM Provider (choose one) LLM_PROVIDER=openai LLM_MODEL=gpt-4o LLM_API_KEY=sk-your-openai-key # For Anthropic: sk-ant-your-key # For Google: your-google-api-key # For Fireworks: your-fireworks-key # Embeddings (required - OpenAI) EMBEDDING_API_KEY=sk-your-openai-key EMBEDDING_MODEL=text-embedding-3-small # Qdrant Vector Database QDRANT_URL={qdrant_url} QDRANT_API_KEY={qdrant_key} QDRANT_COLLECTION={chatbot.get("qdrant_collection_name", "chatbot_collection")} # Server PORT=8000 HOST=0.0.0.0 ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com """ def _main_py(chatbot: Dict): # BUG-14 FIX: Use json.dumps to safely escape system_prompt # Previously used f-string with triple quotes, which broke if prompt contained """ or { safe_name = _escape_for_python(chatbot.get("name", "Chatbot")) safe_prompt = _escape_for_python(chatbot.get("system_prompt") or "You are a helpful assistant.") return f'''""" Auto-generated FastAPI backend for: {chatbot.get("name", "Chatbot")} Generated by Contexta Platform """ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Optional import os from dotenv import load_dotenv from rag_engine import RAGEngine load_dotenv() # BUG-14 FIX: System prompt stored safely via json-escaped string SYSTEM_PROMPT = {safe_prompt} app = FastAPI( title={safe_name} + " API", version="1.0.0" ) app.add_middleware( CORSMiddleware, allow_origins=os.getenv("ALLOWED_ORIGINS", "*").split(","), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) rag = RAGEngine( qdrant_url=os.getenv("QDRANT_URL"), qdrant_api_key=os.getenv("QDRANT_API_KEY"), collection_name=os.getenv("QDRANT_COLLECTION"), llm_provider=os.getenv("LLM_PROVIDER", "openai"), llm_model=os.getenv("LLM_MODEL", "gpt-4o"), llm_api_key=os.getenv("LLM_API_KEY"), embedding_api_key=os.getenv("EMBEDDING_API_KEY"), embedding_model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"), system_prompt=SYSTEM_PROMPT, ) class ChatRequest(BaseModel): message: str session_id: Optional[str] = None language: str = "en" history: List[dict] = [] class Source(BaseModel): document_name: str text: str score: float class ChatResponse(BaseModel): response: str session_id: str sources: List[Source] @app.post("/chat", response_model=ChatResponse) async def chat(request: ChatRequest): import uuid session_id = request.session_id or str(uuid.uuid4()) result = await rag.query( query=request.message, history=request.history, language=request.language, ) return ChatResponse( response=result["response"], session_id=session_id, sources=[Source(**s) for s in result.get("sources", [])], ) @app.get("/health") def health(): return {{"status": "healthy", "chatbot": {safe_name}}} if __name__ == "__main__": import uvicorn uvicorn.run(app, host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", 8000))) ''' def _rag_engine_py(): return '''"""RAG Engine - Retrieval-Augmented Generation""" from qdrant_client import QdrantClient from qdrant_client.http.models import Distance, VectorParams from openai import AsyncOpenAI from typing import List, Dict, Any, Optional import logging logger = logging.getLogger(__name__) class RAGEngine: def __init__(self, qdrant_url, qdrant_api_key, collection_name, llm_provider, llm_model, llm_api_key, embedding_api_key, embedding_model, system_prompt): self.collection_name = collection_name self.llm_provider = llm_provider self.llm_model = llm_model self.llm_api_key = llm_api_key self.system_prompt = system_prompt self.embedding_model = embedding_model # Initialize Qdrant self.qdrant = QdrantClient(url=qdrant_url, api_key=qdrant_api_key) # Initialize embedding client self.embed_client = AsyncOpenAI(api_key=embedding_api_key) async def _get_embedding(self, text: str) -> List[float]: response = await self.embed_client.embeddings.create( model=self.embedding_model, input=text, ) return response.data[0].embedding async def _search_vectors(self, query_embedding: List[float], top_k: int = 5) -> List[Dict]: results = self.qdrant.search( collection_name=self.collection_name, query_vector=query_embedding, limit=top_k, ) return [ { "document_name": r.payload.get("document_name", "Unknown"), "text": r.payload.get("text", ""), "score": r.score, } for r in results ] async def query(self, query: str, history: List[Dict] = None, language: str = "en") -> Dict: # Get embedding for query query_embedding = await self._get_embedding(query) # Search for relevant chunks sources = await self._search_vectors(query_embedding) # Build context from sources context = "\\n\\n".join([ f"[Source: {s['document_name']}]\\n{s['text']}" for s in sources ]) # Build messages messages = [ {"role": "system", "content": f"{self.system_prompt}\\n\\nUse the following context to answer:\\n{context}"}, ] if history: messages.extend(history[-10:]) messages.append({"role": "user", "content": query}) # Generate response based on provider response_text = await self._generate(messages) return { "response": response_text, "sources": sources, } async def _generate(self, messages: List[Dict]) -> str: if self.llm_provider == "openai": from openai import AsyncOpenAI client = AsyncOpenAI(api_key=self.llm_api_key) response = await client.chat.completions.create( model=self.llm_model, messages=messages, max_tokens=1000, ) return response.choices[0].message.content elif self.llm_provider == "anthropic": import anthropic client = anthropic.AsyncAnthropic(api_key=self.llm_api_key) system = messages[0]["content"] msgs = [m for m in messages[1:] if m["role"] in ("user", "assistant")] response = await client.messages.create( model=self.llm_model, max_tokens=1000, system=system, messages=msgs, ) return response.content[0].text elif self.llm_provider == "google": import google.generativeai as genai genai.configure(api_key=self.llm_api_key) model = genai.GenerativeModel(self.llm_model) prompt = "\\n".join([f"{m['role']}: {m['content']}" for m in messages]) response = await model.generate_content_async(prompt) return response.text else: import httpx headers = {"Authorization": f"Bearer {self.llm_api_key}", "Content-Type": "application/json"} async with httpx.AsyncClient(timeout=60) as client: resp = await client.post( "https://api.fireworks.ai/inference/v1/chat/completions", headers=headers, json={"model": self.llm_model, "messages": messages, "max_tokens": 1000}, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] ''' def _dockerfile(): return """FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] """ def _docker_compose(chatbot: Dict): name = chatbot.get("name", "chatbot").lower().replace(" ", "-") return f"""version: "3.8" services: api: build: . container_name: {name}-api ports: - "${{PORT:-8000}}:8000" env_file: - .env restart: unless-stopped """ def _chat_widget_tsx(chatbot: Dict): safe_name = json.dumps(chatbot.get("name", "Chatbot")) safe_welcome = json.dumps(chatbot.get("welcome_message", "Hello! How can I help you?")) color = chatbot.get("primary_color", "#6366f1") return f'''import React, {{ useState }} from "react"; import {{ useChat }} from "./useChat"; export const ChatWidget: React.FC = () => {{ const [isOpen, setIsOpen] = useState(false); const {{ messages, isLoading, sendMessage }} = useChat({safe_welcome}); const [input, setInput] = useState(""); const handleSend = () => {{ if (!input.trim() || isLoading) return; sendMessage(input.trim()); setInput(""); }}; return ( <> {{isOpen && (
{safe_name}
{{messages.map((m, i) => (
{{m.content}}
))}} {{isLoading &&
Typing...
}}
setInput(e.target.value)}} onKeyDown={{e => e.key === "Enter" && handleSend()}} placeholder="Type a message..." style={{{{ flex: 1, border: "1px solid #d1d5db", borderRadius: 8, padding: "6px 10px", fontSize: 13, outline: "none" }}}} />
)}} ); }}; ''' def _use_chat_ts(): return '''import { useState, useCallback } from "react"; import { sendChatMessage } from "./api"; interface Message { role: "user" | "assistant"; content: string; } export function useChat(welcomeMessage: string) { const [messages, setMessages] = useState([ { role: "assistant", content: welcomeMessage } ]); const [isLoading, setIsLoading] = useState(false); const [sessionId] = useState(() => crypto.randomUUID()); const sendMessage = useCallback(async (content: string) => { setMessages(prev => [...prev, { role: "user", content }]); setIsLoading(true); try { const history = messages.map(m => ({ role: m.role, content: m.content })); const result = await sendChatMessage({ message: content, session_id: sessionId, history }); setMessages(prev => [...prev, { role: "assistant", content: result.response }]); } catch { setMessages(prev => [...prev, { role: "assistant", content: "Sorry, I encountered an error. Please try again." }]); } finally { setIsLoading(false); } }, [messages, sessionId]); return { messages, isLoading, sendMessage }; } ''' def _api_ts(): return '''const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"; export async function sendChatMessage(payload: { message: string; session_id: string; history?: any[]; }) { const response = await fetch(`${API_URL}/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) throw new Error("Chat request failed"); return response.json(); } ''' def _types_ts(): return '''export interface Message { role: "user" | "assistant"; content: string; } export interface Source { document_name: string; text: string; score: number; } export interface ChatResponse { response: string; session_id: string; sources: Source[]; } ''' def _package_json(chatbot: Dict): name = chatbot.get("name", "chatbot").lower().replace(" ", "-") # Sanitize name for package.json safe_name = "".join(c for c in name if c.isalnum() or c == "-") return f'''{{ "name": "{safe_name}-widget", "version": "1.0.0", "scripts": {{ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }}, "dependencies": {{ "react": "^18.2.0", "react-dom": "^18.2.0" }}, "devDependencies": {{ "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "typescript": "^5.0.0", "vite": "^5.0.0", "@vitejs/plugin-react": "^4.0.0" }} }} ''' def _tsconfig(): return '''{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM"], "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true } } ''' def _vite_config(): return '''import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], build: { lib: { entry: "src/main.tsx", name: "ChatWidget", fileName: "chatbot-widget" }, rollupOptions: { external: ["react", "react-dom"], } } }); ''' def _backend_readme(chatbot: Dict): return f"""# {chatbot.get("name", "Chatbot")} - Backend API ## Quick Start ```bash cp .env.example .env # Edit .env with your API keys pip install -r requirements.txt uvicorn main:app --reload --port 8000 ``` ## Deploy with Docker ```bash cp .env.example .env # Edit .env docker-compose up -d ``` ## API Endpoints - `POST /chat` - Send a message - `GET /health` - Health check ## Environment Variables See `.env.example` for all required variables. """ def _frontend_readme(chatbot: Dict): return f"""# {chatbot.get("name", "Chatbot")} - Chat Widget ## Quick Start ```bash npm install npm run dev ``` Create a `.env` file: ``` VITE_API_URL=http://localhost:8000 ``` ## Build for Production ```bash npm run build ``` The built files will be in `dist/`. ## Embed in Your Website ```html ``` """ def _quick_start(chatbot: Dict): return f"""# {chatbot.get("name", "Chatbot")} - Quick Start Guide ## Prerequisites - Python 3.11+ - Node.js 18+ (for the widget) ## Step 1: Backend Setup (2 minutes) ```bash cd backend cp .env.example .env # Edit .env with your API keys pip install -r requirements.txt uvicorn main:app --reload ``` Visit http://localhost:8000/health to verify. ## Step 2: Frontend Setup (1 minute) ```bash cd frontend npm install echo "VITE_API_URL=http://localhost:8000" > .env npm run dev ``` ## Step 3: Test It! Open http://localhost:5173 and start chatting! ## Deployment See `backend/README.md` and `frontend/README.md` for deployment guides. """ def _setup_wizard(chatbot: Dict): return f'''#!/usr/bin/env python3 """Interactive setup wizard for {chatbot.get("name", "Chatbot")}""" import os import sys def main(): print("=" * 50) print(f" Setup Wizard: {chatbot.get("name", "Chatbot")}") print("=" * 50) print() env_vars = {{}} # LLM Provider print("Choose your LLM provider:") print(" 1. OpenAI (GPT-4o, GPT-4 Turbo)") print(" 2. Anthropic (Claude 3.5 Sonnet)") print(" 3. Google (Gemini 1.5 Pro)") print(" 4. Fireworks AI (Llama, Mixtral)") choice = input("Enter choice [1]: ").strip() or "1" providers = {{"1": "openai", "2": "anthropic", "3": "google", "4": "fireworks"}} env_vars["LLM_PROVIDER"] = providers.get(choice, "openai") env_vars["LLM_API_KEY"] = input(f"Enter {{env_vars['LLM_PROVIDER']}} API key: ").strip() env_vars["EMBEDDING_API_KEY"] = input("Enter OpenAI API key (for embeddings): ").strip() or env_vars["LLM_API_KEY"] # Write .env env_path = os.path.join("backend", ".env") with open(env_path, "w") as f: for k, v in env_vars.items(): f.write(f"{{k}}={{v}}\\n") print(f"\\nConfiguration saved to {{env_path}}") print("\\nTo start: cd backend && uvicorn main:app --reload") if __name__ == "__main__": main() '''