import zipfile import io 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 """ 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): 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() app = FastAPI( title="{chatbot.get("name", "Chatbot")} 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="""{chatbot.get("system_prompt") or "You are a helpful assistant."}""", ) 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": "{chatbot.get("name", "Chatbot")}"}} 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.embedding_model = embedding_model self.system_prompt = system_prompt # Qdrant qdrant_kwargs = {"url": qdrant_url} if qdrant_api_key: qdrant_kwargs["api_key"] = qdrant_api_key self.qdrant = QdrantClient(**qdrant_kwargs) # OpenAI for embeddings self.embed_client = AsyncOpenAI(api_key=embedding_api_key) async def embed(self, text: str) -> List[float]: resp = await self.embed_client.embeddings.create( model=self.embedding_model, input=text ) return resp.data[0].embedding async def retrieve(self, query_vector: List[float], limit: int = 5) -> List[Dict]: results = self.qdrant.search( collection_name=self.collection_name, query_vector=query_vector, limit=limit, score_threshold=0.3, ) return [{"text": r.payload.get("text", ""), "document_name": r.payload.get("file_name", ""), "score": r.score} for r in results] 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) resp = await client.chat.completions.create( model=self.llm_model, messages=messages, max_tokens=1000 ) return resp.choices[0].message.content elif self.llm_provider == "anthropic": import anthropic client = anthropic.AsyncAnthropic(api_key=self.llm_api_key) system = next((m["content"] for m in messages if m["role"] == "system"), "") conv = [m for m in messages if m["role"] != "system"] resp = await client.messages.create( model=self.llm_model, max_tokens=1000, system=system, messages=conv ) return resp.content[0].text elif self.llm_provider == "fireworks": import httpx async with httpx.AsyncClient(timeout=60) as c: r = await c.post( "https://api.fireworks.ai/inference/v1/chat/completions", headers={"Authorization": f"Bearer {self.llm_api_key}"}, json={"model": self.llm_model, "messages": messages, "max_tokens": 1000}, ) r.raise_for_status() return r.json()["choices"][0]["message"]["content"] return "Error: unknown provider" async def query(self, query: str, history: List[Dict] = None, language: str = "en") -> Dict: if history is None: history = [] query_vec = await self.embed(query) docs = await self.retrieve(query_vec) context = "\\n\\n---\\n\\n".join(d["text"] for d in docs) or "No context found." system = f"{self.system_prompt}\\n\\nContext:\\n{context}" messages = [{"role": "system", "content": system}] for h in history[-10:]: messages.append(h) messages.append({"role": "user", "content": query}) response = await self.generate(messages) return {"response": response, "sources": docs} ''' 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: . ports: - "8000:8000" env_file: .env restart: unless-stopped container_name: {name}-api """ def _chat_widget_tsx(chatbot: Dict): color = chatbot.get("primary_color", "#6366f1") welcome = chatbot.get("welcome_message", "Hello! How can I help you today?") name = chatbot.get("name", "Assistant") return f'''import React, {{ useState, useRef, useEffect }} from "react"; import {{ useChat }} from "./useChat"; const PRIMARY_COLOR = "{color}"; const BOT_NAME = "{name}"; const WELCOME_MESSAGE = "{welcome}"; export const ChatWidget: React.FC = () => {{ const [isOpen, setIsOpen] = useState(false); const {{ messages, isLoading, sendMessage }} = useChat(WELCOME_MESSAGE); const bottomRef = useRef(null); useEffect(() => {{ bottomRef.current?.scrollIntoView({{ behavior: "smooth" }}); }}, [messages]); return ( <> {{isOpen && (
{{BOT_NAME}}
{{messages.map((msg, i) => (
{{msg.content}}
))}} {{isLoading &&
Thinking...
}}
{{ if (e.key === "Enter" && !e.shiftKey) {{ e.preventDefault(); const val = (e.target as HTMLInputElement).value.trim(); if (val) {{ sendMessage(val); (e.target as HTMLInputElement).value = ""; }} }} }}}} />
)}} ); }}; ''' 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(" ", "-") return f'''{{ "name": "{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 cp .env.example .env # Set VITE_API_URL to your backend URL npm install npm run dev ``` ## Build for Production ```bash npm run build ``` ## Embed in Any Website ```html ``` ## Environment Variables - `VITE_API_URL` - Backend API URL (default: http://localhost:8000) """ def _quick_start(chatbot: Dict): return f"""# Quick Start - {chatbot.get("name", "Chatbot")} Get your chatbot running in 5 minutes! ## Prerequisites - Python 3.11+ - Node.js 18+ - API key from OpenAI, Anthropic, or Google ## 1. Configure Environment (2 min) Run the setup wizard: ```bash python setup.py ``` Or manually: ```bash cd backend cp .env.example .env # Edit .env with your keys ``` ## 2. Start Backend (1 min) ```bash cd backend pip install -r requirements.txt uvicorn main:app --reload ``` Backend runs at: http://localhost:8000 ## 3. Start Frontend Widget (1 min) ```bash cd frontend npm install npm run dev ``` Widget available at: http://localhost:3000 ## 4. Embed in Your Website After building (`npm run build`): ```html ``` ## Deploy ### Railway (Recommended) ```bash railway init railway up ``` ### Docker ```bash cd backend && docker-compose up -d ``` """ def _setup_wizard(chatbot: Dict): return f'''#!/usr/bin/env python3 """ Interactive setup wizard for {chatbot.get("name", "Chatbot")} """ import os from pathlib import Path def main(): print(""" ╔══════════════════════════════════════╗ ║ {chatbot.get("name", "Chatbot")} Setup Wizard ║ ╚══════════════════════════════════════╝ """) print("Choose your LLM provider:") print("1. OpenAI (GPT-4o)") print("2. Anthropic (Claude)") print("3. Google (Gemini)") print("4. Fireworks AI (Free, open-source models)") choice = input("\\nEnter choice (1-4): ").strip() providers = {{"1": "openai", "2": "anthropic", "3": "google", "4": "fireworks"}} models = {{"1": "gpt-4o", "2": "claude-3-5-sonnet-20241022", "3": "gemini-1.5-pro", "4": "accounts/fireworks/models/llama-v3p1-70b-instruct"}} provider = providers.get(choice, "openai") model = models.get(choice, "gpt-4o") api_key = input(f"Enter your {{provider}} API key: ").strip() env_content = f"""LLM_PROVIDER={{provider}} LLM_MODEL={{model}} LLM_API_KEY={{api_key}} EMBEDDING_API_KEY={{api_key if provider == "openai" else input("Enter OpenAI key for embeddings: ").strip()}} EMBEDDING_MODEL=text-embedding-3-small QDRANT_URL={os.getenv("QDRANT_URL", "your-qdrant-url")} QDRANT_API_KEY={os.getenv("QDRANT_API_KEY", "your-qdrant-key")} QDRANT_COLLECTION={chatbot.get("qdrant_collection_name", "chatbot_collection")} """ env_file = Path("backend/.env") env_file.write_text(env_content) print("\\n✅ .env file created!") frontend_url = input("\\nBackend URL for frontend (default: http://localhost:8000): ").strip() if not frontend_url: frontend_url = "http://localhost:8000" Path("frontend/.env").write_text(f"VITE_API_URL={{frontend_url}}\\n") print("✅ Frontend .env created!") print(""" \\n╔══════════════════════════════════════╗ ║ Setup Complete! 🎉 ║ ╠══════════════════════════════════════╣ ║ Backend: cd backend && uvicorn ║ ║ main:app --reload ║ ║ Frontend: cd frontend && npm dev ║ ╚══════════════════════════════════════╝ """) if __name__ == "__main__": main() '''