Initial commit

This commit is contained in:
belviskhoremk
2026-02-22 21:59:37 +00:00
commit 5bd496d355
27 changed files with 4172 additions and 0 deletions

713
app/services/code_export.py Normal file
View File

@@ -0,0 +1,713 @@
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()
'''