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 && (