Files
contexta_be/app/services/code_export.py
belviskhoremk 5bd496d355 Initial commit
2026-02-22 21:59:37 +00:00

714 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HTMLDivElement>(null);
useEffect(() => {{
bottomRef.current?.scrollIntoView({{ behavior: "smooth" }});
}}, [messages]);
return (
<>
{{isOpen && (
<div style={{{{
position: "fixed", bottom: 90, right: 20, width: 360, height: 520,
borderRadius: 16, boxShadow: "0 20px 60px rgba(0,0,0,0.2)",
display: "flex", flexDirection: "column", background: "#fff",
fontFamily: "system-ui, -apple-system, sans-serif", zIndex: 9999
}}}}>
<div style={{{{ background: PRIMARY_COLOR, padding: "16px 20px",
borderRadius: "16px 16px 0 0", display: "flex", justifyContent: "space-between", alignItems: "center" }}}}>
<span style={{{{ color: "#fff", fontWeight: 600, fontSize: 16 }}}}>{{BOT_NAME}}</span>
<button onClick={{() => setIsOpen(false)}}
style={{{{ background: "none", border: "none", color: "#fff", cursor: "pointer", fontSize: 20 }}}}>×</button>
</div>
<div style={{{{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 12 }}}}>
{{messages.map((msg, i) => (
<div key={{i}} style={{{{ display: "flex", justifyContent: msg.role === "user" ? "flex-end" : "flex-start" }}}}>
<div style={{{{
maxWidth: "80%", padding: "10px 14px", borderRadius: 12, fontSize: 14, lineHeight: 1.5,
background: msg.role === "user" ? PRIMARY_COLOR : "#f3f4f6",
color: msg.role === "user" ? "#fff" : "#111"
}}}}>{{msg.content}}</div>
</div>
))}}
{{isLoading && <div style={{{{ color: "#6b7280", fontSize: 13 }}}}>Thinking...</div>}}
<div ref={{bottomRef}} />
</div>
<div style={{{{ padding: "12px 16px", borderTop: "1px solid #e5e7eb", display: "flex", gap: 8 }}}}>
<input
style={{{{ flex: 1, border: "1px solid #e5e7eb", borderRadius: 8, padding: "8px 12px", outline: "none", fontSize: 14 }}}}
placeholder="Type a message..."
onKeyDown={{(e) => {{
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 = ""; }}
}}
}}}}
/>
<button
style={{{{ background: PRIMARY_COLOR, color: "#fff", border: "none", borderRadius: 8,
padding: "8px 14px", cursor: "pointer", fontSize: 14 }}}}
onClick={{(e) => {{
const input = (e.currentTarget.previousSibling as HTMLInputElement);
const val = input.value.trim();
if (val) {{ sendMessage(val); input.value = ""; }}
}}}}
>Send</button>
</div>
</div>
)}}
<button
onClick={{() => setIsOpen(!isOpen)}}
style={{{{
position: "fixed", bottom: 20, right: 20, width: 56, height: 56,
borderRadius: "50%", background: PRIMARY_COLOR, border: "none",
cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
boxShadow: "0 4px 20px rgba(0,0,0,0.2)", zIndex: 9999, fontSize: 24
}}}}
>
{{isOpen ? "×" : "💬"}}
</button>
</>
);
}};
'''
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<Message[]>([
{ 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
<script src="path/to/dist/chatbot-widget.umd.cjs"></script>
```
## 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
<script src="dist/chatbot-widget.umd.cjs"></script>
```
## 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()
'''