2055 lines
56 KiB
Python
2055 lines
56 KiB
Python
import os, json, uuid
|
|
import requests
|
|
from flask import Flask, Response, request, jsonify, session, redirect, url_for, send_file
|
|
from authlib.integrations.flask_client import OAuth
|
|
from functools import wraps
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from markdown import markdown
|
|
|
|
from config import Config
|
|
from models import Base, Conversation, Message, Attachment
|
|
|
|
from flask_sock import Sock
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
app.secret_key = app.config["SECRET_KEY"]
|
|
|
|
engine = create_engine(os.getenv("DATABASE_URL"))
|
|
SessionLocal = sessionmaker(bind=engine)
|
|
Base.metadata.create_all(engine)
|
|
|
|
# TODO:
|
|
# conversation.user_email == email
|
|
# msgs = db.query(Message).filter_by(conversation_id=cid).all()
|
|
|
|
|
|
sock = Sock(app)
|
|
|
|
@sock.route("/ws/notifications")
|
|
def notifications(ws):
|
|
# All comments are in English.
|
|
# Simple example: send a ping every time a message is added.
|
|
while True:
|
|
data = ws.receive()
|
|
# You can interpret data or just echo.
|
|
ws.send(json.dumps({"type": "pong", "payload": data}))
|
|
|
|
|
|
@celery.task(name="tasks.admin_generate", bind=True, max_retries=3)
|
|
def admin_generate(self, conversation_id, model, prompt):
|
|
# Same logic as generate_task, but runs in the admin queue
|
|
...
|
|
|
|
@app.route("/api/queue/admin_generate", methods=["POST"])
|
|
@login_required
|
|
@admin_required
|
|
def queue_admin_generate():
|
|
data = request.json
|
|
cid = data["conversation_id"]
|
|
model = data["model"]
|
|
prompt = data["prompt"]
|
|
|
|
task = admin_generate.delay(cid, model, prompt)
|
|
return jsonify({"task_id": task.id})
|
|
|
|
|
|
# ================
|
|
# DECORATORS
|
|
# ================
|
|
|
|
|
|
def enforce_plan_limits(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
db = SessionLocal()
|
|
|
|
if app.config["AUTH_MODE"] == "oauth":
|
|
email = session["user"]["email"]
|
|
else:
|
|
email = session["db_user"]["email"]
|
|
|
|
user = db.query(User).filter_by(email=email).first()
|
|
plan = db.query(Plan).filter_by(id=user.plan_id).first()
|
|
|
|
# Limite de conversas
|
|
conv_count = db.query(Conversation).filter_by(user_email=email).count()
|
|
if conv_count >= plan.max_conversations:
|
|
db.close()
|
|
return jsonify({"erro": "Você atingiu o limite de conversas do seu plano."}), 403
|
|
|
|
db.close()
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
def require_subscription(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
db = SessionLocal()
|
|
|
|
if app.config["AUTH_MODE"] == "oauth":
|
|
email = session["user"]["email"]
|
|
else:
|
|
email = session["db_user"]["email"]
|
|
|
|
user = db.query(User).filter_by(email=email).first()
|
|
plan = db.query(Plan).filter_by(id=user.plan_id).first()
|
|
sub = db.query(Subscription).filter_by(user_email=email, status="active").first()
|
|
|
|
db.close()
|
|
|
|
if not sub:
|
|
return jsonify({"erro": "Sua assinatura expirou ou não está ativa."}), 402
|
|
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
def make_cache_key(model, prompt):
|
|
raw = f"{email}:{model}:{prompt}"
|
|
return hashlib.sha256(raw.encode()).hexdigest()
|
|
|
|
def get_cache(model, prompt):
|
|
key = make_cache_key(model, prompt)
|
|
db = SessionLocal()
|
|
entry = db.query(Cache).filter_by(key=key).first()
|
|
db.close()
|
|
return entry.response if entry else None
|
|
|
|
def set_cache(model, prompt, response):
|
|
key = make_cache_key(model, prompt)
|
|
db = SessionLocal()
|
|
entry = Cache(key=key, model=model, response=response)
|
|
db.add(entry)
|
|
db.commit()
|
|
db.close()
|
|
|
|
|
|
import structlog
|
|
logger = structlog.get_logger()
|
|
@app.before_request
|
|
def log_request():
|
|
logger.info(
|
|
"request",
|
|
path=request.path,
|
|
method=request.method,
|
|
remote=request.remote_addr,
|
|
)
|
|
|
|
@app.after_request
|
|
def log_response(response):
|
|
logger.info(
|
|
"response",
|
|
path=request.path,
|
|
status=response.status_code,
|
|
)
|
|
return response
|
|
|
|
|
|
from limits import RateLimitItemPerMinute
|
|
from time import time
|
|
|
|
rate_store = {}
|
|
|
|
def rate_limited(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
if app.config["AUTH_MODE"] == "oauth":
|
|
email = session["user"]["email"]
|
|
else:
|
|
email = session["db_user"]["email"]
|
|
|
|
key = f"{email}:{f.__name__}"
|
|
limit = RateLimitItemPerMinute(30) # 30 req/min por endpoint
|
|
|
|
now = int(time())
|
|
window = now // 60
|
|
|
|
used = rate_store.get((key, window), 0)
|
|
if used >= limit.amount:
|
|
return jsonify({"error": "Rate limit exceeded"}), 429
|
|
|
|
rate_store[(key, window)] = used + 1
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
def call_ollama_with_fallback(payload):
|
|
for model in FALLBACK_MODELS:
|
|
payload["model"] = model
|
|
try:
|
|
r = requests.post(f"{app.config['OLLAMA_BASE_URL']}/api/generate", json=payload)
|
|
r.raise_for_status()
|
|
return r.json(), model
|
|
except Exception:
|
|
continue
|
|
raise Exception("All models failed")
|
|
|
|
def corp_admin_required(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
db = SessionLocal()
|
|
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
u = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
if not u or u.priority not in ["system_admin", "corp_admin"]:
|
|
return jsonify({"erro": "Acesso restrito ao administrador corporativo."}), 403
|
|
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
def system_admin_required(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
db = SessionLocal()
|
|
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
u = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
if not u or u.priority != "system_admin":
|
|
return jsonify({"erro": "Acesso restrito ao administrador do sistema."}), 403
|
|
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
def admin_required(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
db = SessionLocal()
|
|
|
|
if app.config["AUTH_MODE"] == "oauth":
|
|
email = session["user"]["email"]
|
|
else:
|
|
email = session["db_user"]["email"]
|
|
|
|
u = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
if not u or u.role != "admin":
|
|
return jsonify({"error": "Admin only"}), 403
|
|
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
|
|
def require_capability(cap):
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
db = SessionLocal()
|
|
email = session["user"]["email"]
|
|
u = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
if not u or not getattr(u, cap, False):
|
|
return jsonify({"error": "Forbidden", "missing_capability": cap}), 403
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
@app.before_request
|
|
def enforce_system_language():
|
|
# All internal system logs/messages must be in English.
|
|
logger.info(
|
|
"request_received",
|
|
path=request.path,
|
|
method=request.method,
|
|
lang=Config.SYSTEM_LANG
|
|
)
|
|
|
|
|
|
|
|
|
|
# ================
|
|
# AUTH
|
|
# ================
|
|
|
|
|
|
import smtplib
|
|
from email.message import EmailMessage
|
|
import uuid
|
|
|
|
@app.route("/auth/forgot", methods=["POST"])
|
|
def forgot():
|
|
email = request.form.get("email")
|
|
|
|
db = SessionLocal()
|
|
user = db.query(User).filter_by(email=email).first()
|
|
if not user:
|
|
db.close()
|
|
return jsonify({"status": "ok"}) # não revela nada
|
|
|
|
token = str(uuid.uuid4())
|
|
user.reset_token = token
|
|
db.commit()
|
|
db.close()
|
|
|
|
msg = EmailMessage()
|
|
msg["Subject"] = "Password reset"
|
|
msg["From"] = Config.SMTP_USER
|
|
msg["To"] = email
|
|
msg.set_content(f"Reset link: https://{Config.APP_DOMAIN}/auth/reset?token={token}")
|
|
|
|
with smtplib.SMTP(Config.SMTP_HOST, Config.SMTP_PORT) as s:
|
|
s.starttls()
|
|
s.login(Config.SMTP_USER, Config.SMTP_PASS)
|
|
s.send_message(msg)
|
|
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@app.route("/auth/reset", methods=["POST"])
|
|
def reset():
|
|
token = request.form.get("token")
|
|
new_password = request.form.get("password")
|
|
|
|
db = SessionLocal()
|
|
user = db.query(User).filter_by(reset_token=token).first()
|
|
if not user:
|
|
db.close()
|
|
return jsonify({"error": "Invalid token"}), 400
|
|
|
|
user.password = new_password
|
|
user.reset_token = None
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
import pyotp, base64, os
|
|
|
|
@app.route("/auth/enable_2fa", methods=["POST"])
|
|
@login_required
|
|
def enable_2fa():
|
|
db = SessionLocal()
|
|
if app.config["AUTH_MODE"] == "oauth":
|
|
email = session["user"]["email"]
|
|
else:
|
|
email = session["db_user"]["email"]
|
|
|
|
u = db.query(User).filter_by(email=email).first()
|
|
secret = base64.b32encode(os.urandom(10)).decode("utf-8")
|
|
u.totp_secret = secret
|
|
u.twofa_enabled = True
|
|
db.commit()
|
|
db.close()
|
|
|
|
# Você mostra esse secret como QR code no frontend (otpauth:// URI)
|
|
return jsonify({"secret": secret})
|
|
|
|
|
|
@app.route("/auth_mode")
|
|
def auth_mode():
|
|
return jsonify({"mode": app.config["AUTH_MODE"]})
|
|
|
|
@app.route("/auth/login", methods=["GET", "POST"])
|
|
def db_login():
|
|
if request.method == "GET":
|
|
return send_file("/srv/auth/login.html")
|
|
|
|
email = request.form.get("email")
|
|
password = request.form.get("password")
|
|
token = request.form.get("token") # código 2FA
|
|
|
|
db = SessionLocal()
|
|
user = db.query(User).filter_by(email=email).first()
|
|
|
|
if not user or user.password != password:
|
|
db.close()
|
|
return jsonify({"error": "Invalid credentials"}), 401
|
|
|
|
if user.twofa_enabled:
|
|
totp = pyotp.TOTP(user.totp_secret)
|
|
if not totp.verify(token):
|
|
db.close()
|
|
return jsonify({"error": "Invalid 2FA token"}), 401
|
|
|
|
db.close()
|
|
session["db_user"] = {"email": user.email, "name": user.name, "role": user.role}
|
|
return redirect("/")
|
|
|
|
|
|
|
|
oauth = OAuth(app)
|
|
google = oauth.register(
|
|
name="google",
|
|
client_id=app.config["GOOGLE_CLIENT_ID"],
|
|
client_secret=app.config["GOOGLE_CLIENT_SECRET"],
|
|
access_token_url="https://oauth2.googleapis.com/token",
|
|
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
|
api_base_url="https://www.googleapis.com/oauth2/v2/",
|
|
client_kwargs={"scope": "openid email profile"},
|
|
)
|
|
|
|
|
|
def login_required(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
key = "user" if app.config["AUTH_MODE"] == "oauth" else "db_user"
|
|
if key not in session:
|
|
return redirect("/auth/login")
|
|
|
|
email = session[key]["email"]
|
|
db = SessionLocal()
|
|
u = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
if not u:
|
|
session.pop(key, None)
|
|
return redirect("/auth/login")
|
|
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/auth/login", methods=["GET", "POST"])
|
|
def db_login():
|
|
if request.method == "GET":
|
|
return send_file("/srv/auth/login.html")
|
|
|
|
email = request.form.get("email")
|
|
password = request.form.get("password")
|
|
|
|
db = SessionLocal()
|
|
user = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
if not user or user.password != password:
|
|
return jsonify({"error": "Invalid credentials"}), 401
|
|
|
|
session["db_user"] = {
|
|
"email": user.email,
|
|
"name": user.name,
|
|
"role": user.role
|
|
}
|
|
|
|
return redirect("/")
|
|
|
|
@app.route("/auth/logout")
|
|
def db_logout():
|
|
session.pop("db_user", None)
|
|
return redirect("/auth/login")
|
|
|
|
|
|
@app.route("/auth/register", methods=["POST"])
|
|
def db_register():
|
|
email = request.form.get("email")
|
|
password = request.form.get("password")
|
|
name = request.form.get("name")
|
|
|
|
db = SessionLocal()
|
|
if db.query(User).filter_by(email=email).first():
|
|
return jsonify({"error": "User exists"}), 400
|
|
|
|
consent = request.form.get("consent_training") == "on"
|
|
u = User(
|
|
email=email,
|
|
name=name,
|
|
password=password,
|
|
role="user",
|
|
consent_training=consent,
|
|
can_use_audio=False,
|
|
can_use_vision=False,
|
|
can_share=True,
|
|
can_import_docs=True,
|
|
can_export_pdf=True,
|
|
can_export_md=True,
|
|
)
|
|
|
|
from datetime import datetime
|
|
|
|
now = datetime.utcnow()
|
|
u.terms_accepted_at = now
|
|
u.privacy_accepted_at = now
|
|
u.dpa_accepted_at = now
|
|
u.cookies_accepted_at = now
|
|
u.security_accepted_at = now
|
|
|
|
|
|
db.add(u)
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@app.route("/login")
|
|
def login():
|
|
return google.authorize_redirect(app.config["GOOGLE_REDIRECT_URI"])
|
|
|
|
@app.route("/oauth/callback")
|
|
def oauth_callback():
|
|
token = google.authorize_access_token()
|
|
user_info = google.get("userinfo").json()
|
|
#session["user"] = user_info
|
|
db = SessionLocal()
|
|
u = db.query(User).filter_by(email=user_info["email"]).first()
|
|
if not u:
|
|
u = User(
|
|
email=user_info["email"],
|
|
name=user_info.get("name", ""),
|
|
role="user",
|
|
can_use_audio=False,
|
|
can_use_vision=False,
|
|
can_share=True,
|
|
)
|
|
db.add(u)
|
|
db.commit()
|
|
db.close()
|
|
return redirect("/")
|
|
|
|
|
|
# ================
|
|
# LEGAL
|
|
# ================
|
|
|
|
@app.route("/legal/links")
|
|
def legal_links():
|
|
return jsonify({
|
|
"termos": "/legal/termos-de-uso.html",
|
|
"privacidade": "/legal/politica-privacidade.html",
|
|
"dpa": "/legal/dpa.html",
|
|
"cookies": "/legal/politica-cookies.html",
|
|
"seguranca": "/legal/politica-seguranca.html",
|
|
"consentimento": "/legal/consentimento-treinamento.html",
|
|
})
|
|
|
|
# ================
|
|
# ADMIN
|
|
# ================
|
|
|
|
@app.route("/admin/set_priority/<email>", methods=["POST"])
|
|
@login_required
|
|
@system_admin_required
|
|
def set_priority(email):
|
|
data = request.json
|
|
priority = data.get("priority")
|
|
|
|
if priority not in ["system_admin", "corp_admin", "user"]:
|
|
return jsonify({"erro": "Prioridade inválida."}), 400
|
|
|
|
db = SessionLocal()
|
|
u = db.query(User).filter_by(email=email).first()
|
|
u.priority = priority
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
|
|
@app.route("/admin/plans")
|
|
@login_required
|
|
@admin_required
|
|
def admin_plans():
|
|
db = SessionLocal()
|
|
plans = db.query(Plan).all()
|
|
db.close()
|
|
return jsonify([{
|
|
"id": p.id,
|
|
"name": p.name,
|
|
"price_monthly": p.price_monthly,
|
|
"price_yearly": p.price_yearly,
|
|
"priority_queue": p.priority_queue
|
|
} for p in plans])
|
|
|
|
@app.route("/admin/plans/create", methods=["POST"])
|
|
@login_required
|
|
@admin_required
|
|
def admin_create_plan():
|
|
data = request.json
|
|
db = SessionLocal()
|
|
p = Plan(**data)
|
|
db.add(p)
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"status": "ok"})
|
|
|
|
@app.route("/admin/subscriptions/<email>")
|
|
@login_required
|
|
@admin_required
|
|
def admin_subscriptions(email):
|
|
db = SessionLocal()
|
|
subs = db.query(Subscription).filter_by(user_email=email).all()
|
|
db.close()
|
|
return jsonify([{
|
|
"id": s.id,
|
|
"plan_id": s.plan_id,
|
|
"status": s.status,
|
|
"renew_at": s.renew_at.isoformat()
|
|
} for s in subs])
|
|
|
|
|
|
@app.route("/admin/users")
|
|
@login_required
|
|
@admin_required
|
|
def admin_users():
|
|
db = SessionLocal()
|
|
#users = db.query(User).all()
|
|
users = db.query(User).filter_by(company_id=u.company_id)
|
|
data = [{
|
|
"email": u.email,
|
|
"name": u.name,
|
|
"role": u.role,
|
|
"can_use_audio": u.can_use_audio,
|
|
"can_use_vision": u.can_use_vision,
|
|
"can_share": u.can_share,
|
|
"can_import_docs": u.can_import_docs,
|
|
"can_export_pdf": u.can_export_pdf,
|
|
"can_export_md": u.can_export_md,
|
|
} for u in users]
|
|
db.close()
|
|
return jsonify(data)
|
|
|
|
@app.route("/admin/update_acl/<email>", methods=["POST"])
|
|
@login_required
|
|
@admin_required
|
|
def update_acl(email):
|
|
db = SessionLocal()
|
|
u = db.query(User).filter_by(email=email).first()
|
|
if not u:
|
|
return jsonify({"error": "User not found"}), 404
|
|
|
|
data = request.json
|
|
for key, value in data.items():
|
|
if hasattr(u, key):
|
|
setattr(u, key, value)
|
|
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@app.route("/admin/promote/<email>", methods=["POST"])
|
|
@login_required
|
|
@admin_required
|
|
def promote(email):
|
|
db = SessionLocal()
|
|
u = db.query(User).filter_by(email=email).first()
|
|
if not u:
|
|
return jsonify({"error": "User not found"}), 404
|
|
|
|
u.role = "admin"
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"status": "ok"})
|
|
|
|
@app.route("/admin/conversations/<email>")
|
|
@login_required
|
|
@admin_required
|
|
def admin_conversations(email):
|
|
db = SessionLocal()
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
convs = db.query(Conversation).filter_by(user_email=email).all()
|
|
if not convs:
|
|
return jsonify({"erro": "Conversas não encontradas."}), 404
|
|
data = [{"id": c.id, "title": c.title, "tags": c.tags} for c in convs]
|
|
db.close()
|
|
return jsonify(data)
|
|
|
|
|
|
# ================
|
|
# API
|
|
# ================
|
|
|
|
@app.route("/api/chat", methods=["POST"])
|
|
@login_required
|
|
@rate_limited
|
|
def chat():
|
|
data = request.json
|
|
model = data.get("model", "llama3.2")
|
|
messages = data.get("messages", [])
|
|
|
|
url = f"{app.config['OLLAMA_BASE_URL']}/api/chat"
|
|
payload = {"model": model, "messages": messages}
|
|
|
|
r = requests.post(url, json=payload)
|
|
r.raise_for_status()
|
|
|
|
return jsonify(r.json())
|
|
|
|
@app.route("/api/chat/stream")
|
|
@login_required
|
|
@rate_limited
|
|
def chat_stream():
|
|
|
|
cid = request.args.get("conversation_id")
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
|
|
db = SessionLocal()
|
|
msgs = (
|
|
db.query(Message)
|
|
.join(Conversation, Message.conversation_id == Conversation.id)
|
|
.filter(Conversation.id == cid, Conversation.user_email == email)
|
|
.order_by(Message.created_at)
|
|
.all()
|
|
)
|
|
db.close()
|
|
|
|
chatml = [{"role": m.role, "content": m.content} for m in msgs]
|
|
# segue chamada ao Ollama
|
|
|
|
model = request.args.get("model", "llama3.2")
|
|
#cid = request.args.get("conversation_id")
|
|
|
|
def generate():
|
|
url = f"{app.config['OLLAMA_BASE_URL']}/api/chat"
|
|
payload = {"model": model, "messages": chatml, "stream": True}
|
|
|
|
with requests.post(url, json=payload, stream=True) as r:
|
|
for line in r.iter_lines():
|
|
if not line:
|
|
continue
|
|
data = json.loads(line.decode())
|
|
token = data.get("message", {}).get("content", "")
|
|
yield f"data: {json.dumps({'token': token})}\n\n"
|
|
|
|
return Response(generate(), mimetype="text/event-stream")
|
|
|
|
|
|
|
|
|
|
@app.route("/api/new_conversation", methods=["POST"])
|
|
@login_required
|
|
def new_conversation():
|
|
db = SessionLocal()
|
|
cid = str(uuid.uuid4())
|
|
title = request.json.get("title", "New conversation")
|
|
conv = Conversation(id=cid, title=title)
|
|
db.add(conv)
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"conversation_id": cid, "title": title})
|
|
|
|
@app.route("/api/conversations")
|
|
@login_required
|
|
def list_conversations():
|
|
db = SessionLocal()
|
|
user_email = session["user"]["email"]
|
|
convs = db.query(Conversation).filter(
|
|
(Conversation.user_email == user_email) |
|
|
(Conversation.shared == user_email)
|
|
).all()
|
|
data = [{"id": c.id, "title": c.title, "tags": c.tags} for c in convs]
|
|
db.close()
|
|
return jsonify(data)
|
|
|
|
|
|
@app.route("/api/export/md/<cid>/<filename>")
|
|
@login_required
|
|
def export_md(cid, filename):
|
|
# validação de conversa + usuário
|
|
|
|
db = SessionLocal()
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
conv = db.query(Conversation).filter_by(id=cid, user_email=email).first()
|
|
if not conv:
|
|
return jsonify({"erro": "Conversa não encontrada."}), 404
|
|
|
|
msgs = conv.messages
|
|
|
|
md = f"# {conv.title}\n\n"
|
|
if conv.tags:
|
|
md += f"**Tags:** {conv.tags}\n\n"
|
|
|
|
for m in msgs:
|
|
md += f"### {m.role.capitalize()}\n\n{m.content}\n\n"
|
|
|
|
db.close()
|
|
#return Response(md, mimetype="text/markdown")
|
|
return send_file(md_path, as_attachment=True, download_name=filename)
|
|
|
|
|
|
@app.route("/api/file/<file_id>/<filename>")
|
|
@login_required
|
|
def get_file(file_id, filename):
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
|
|
db = SessionLocal()
|
|
f = db.query(File).filter_by(id=file_id, user_email=email).first()
|
|
db.close()
|
|
|
|
if not f:
|
|
return jsonify({"erro": "Arquivo não encontrado."}), 404
|
|
|
|
# Segurança: impedir que o usuário tente baixar outro nome
|
|
if filename != f.original_name:
|
|
return jsonify({"erro": "Nome de arquivo inválido."}), 400
|
|
|
|
return send_file(
|
|
f.path,
|
|
as_attachment=True,
|
|
download_name=f.original_name
|
|
)
|
|
|
|
|
|
from weasyprint import HTML
|
|
|
|
import uuid
|
|
import pypandoc
|
|
from pdfminer.high_level import extract_text
|
|
from docx import Document
|
|
from io.pandoc import convert_document
|
|
|
|
@app.route("/api/import/<cid>", methods=["POST"])
|
|
@login_required
|
|
@rate_limited
|
|
def import_file(cid):
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"erro": "Nenhum arquivo enviado"}), 400
|
|
|
|
email = session["db_user"]["email"] if app.config["AUTH_MODE"] == "db" else session["user"]["email"]
|
|
|
|
file_id = str(uuid.uuid4())
|
|
filename = file.filename
|
|
path = f"/data/{file_id}_{filename}"
|
|
file.save(path)
|
|
|
|
ext = file.filename.lower().split(".")[-1]
|
|
if ext not in ["doc", "docx", "odt", "txt", "md", "html", "htm", "rtf", "epub"]:
|
|
return jsonify({"erro": "Formato não suportado."}), 400
|
|
|
|
if ext == "pdf":
|
|
text = extract_text(path)
|
|
elif ext == "docx":
|
|
doc = Document(path)
|
|
text = "\n".join([p.text for p in doc.paragraphs])
|
|
elif ext == "doc":
|
|
text = pypandoc.convert_file(path, "md")
|
|
else:
|
|
return jsonify({"error": "Unsupported format"}), 400
|
|
|
|
md = pypandoc.convert_text(text, "md", format="plain")
|
|
|
|
db = SessionLocal()
|
|
db.add(File(id=file_id, user_email=email, path=path, original_name=filename))
|
|
db.commit()
|
|
msg = Message(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
role="user",
|
|
content=f"Arquivo importado: {filename}\n```\n{md}\n```"
|
|
)
|
|
db.add(msg)
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok", "filename": filename,"imported_as_markdown": True})
|
|
|
|
@app.route("/api/export/pandoc/<writer>/<cid>/<filename>")
|
|
@login_required
|
|
def export_pandoc(writer, cid, filename):
|
|
if not writer in ["docx","xlsx","pptx","markdown","txt","odt","rtf","epub","html"]:
|
|
return jsonify({"erro": "Formato de exportação não suportado."}), 400
|
|
email = session["db_user"]["email"] if app.config["AUTH_MODE"] == "db" else session["user"]["email"]
|
|
db = SessionLocal()
|
|
msgs = (
|
|
db.query(Message)
|
|
.join(Conversation)
|
|
.filter(Conversation.id == cid, Conversation.user_email == email)
|
|
.order_by(Message.created_at)
|
|
.all()
|
|
)
|
|
db.close()
|
|
|
|
text = "\n".join([m.content for m in msgs])
|
|
|
|
out = f"/data/{uuid.uuid4()}_{filename}"
|
|
pypandoc.convert_text(text, writer, format="plain", outputfile=out)
|
|
|
|
return send_file(out, as_attachment=True, download_name=filename)
|
|
|
|
|
|
@app.route("/api/export/pdf/<cid>/<filename>")
|
|
@login_required
|
|
def export_pdf(cid, filename):
|
|
db = SessionLocal()
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
conv = db.query(Conversation).filter_by(id=cid, user_email=email).first()
|
|
if not conv:
|
|
return jsonify({"erro": "Conversa não encontrada."}), 404
|
|
msgs = conv.messages
|
|
|
|
html = "<h1>{}</h1>".format(conv.title)
|
|
if conv.tags:
|
|
html += "<p><strong>Tags:</strong> {}</p>".format(conv.tags)
|
|
|
|
for m in msgs:
|
|
html += "<h3>{}</h3><p>{}</p>".format(m.role.capitalize(), m.content)
|
|
|
|
db.close()
|
|
|
|
pdf = HTML(string=html).write_pdf()
|
|
#return Response(pdf, mimetype="application/pdf")
|
|
return send_file(pdf_path, as_attachment=True, download_name=filename)
|
|
|
|
|
|
|
|
@app.route("/api/history/<cid>")
|
|
@login_required
|
|
@rate_limited
|
|
def history(cid):
|
|
db = SessionLocal()
|
|
#msgs = db.query(Message).filter_by(conversation_id=cid, user_email=email).order_by(Message.created_at).all()
|
|
msgs = db.query(Message).join(Conversation).filter(
|
|
Conversation.id == cid,
|
|
Conversation.user_email == email
|
|
).all()
|
|
data = [{"role": m.role, "content": markdown(m.content)} for m in msgs]
|
|
db.close()
|
|
return jsonify(data)
|
|
|
|
@app.route("/api/attachment/<cid>", methods=["POST"])
|
|
@login_required
|
|
@rate_limited
|
|
def upload_attachment(cid):
|
|
db = SessionLocal()
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"error": "No file"}), 400
|
|
content = file.read().decode("latin1")
|
|
att = Attachment(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
filename=file.filename,
|
|
mime_type=file.mimetype,
|
|
data=content,
|
|
)
|
|
db.add(att)
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"status": "ok"})
|
|
|
|
@app.route("/api/attachments/<cid>")
|
|
@login_required
|
|
@rate_limited
|
|
def list_attachments(cid):
|
|
db = SessionLocal()
|
|
atts = db.query(Attachment).filter_by(conversation_id=cid, user_email=email).all()
|
|
data = [{"id": a.id, "filename": a.filename} for a in atts]
|
|
db.close()
|
|
return jsonify(data)
|
|
|
|
|
|
@app.route("/api/download/<att_id>")
|
|
@login_required
|
|
@rate_limited
|
|
def download(att_id):
|
|
db = SessionLocal()
|
|
att = db.query(Attachment).filter_by(id=att_id, user_email=email).first()
|
|
db.close()
|
|
|
|
if not att:
|
|
return "Not found", 404
|
|
|
|
return send_file(att.path, as_attachment=True, download_name=att.filename)
|
|
|
|
|
|
|
|
@app.route("/api/file/<file_id>")
|
|
@login_required
|
|
def get_file(file_id):
|
|
db = SessionLocal()
|
|
f = db.query(File).filter_by(id=file_id, user_email=email).first()
|
|
if not f:
|
|
return jsonify({"erro": "Arquivo não encontrado"}), 404
|
|
return send_file(f.path)
|
|
|
|
|
|
@app.route("/api/stream/<cid>")
|
|
@login_required
|
|
def stream(cid):
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
|
|
db = SessionLocal()
|
|
conv = db.query(Conversation).filter_by(id=cid, user_email=email).first()
|
|
db.close()
|
|
|
|
if not conv:
|
|
return jsonify({"erro": "Conversa não encontrada."}), 404
|
|
|
|
# segue streaming normalmente
|
|
# TODO
|
|
...
|
|
|
|
|
|
|
|
@app.route("/api/stream")
|
|
@login_required
|
|
@rate_limited
|
|
def stream():
|
|
cid = request.args.get("conversation_id")
|
|
model = request.args.get("model", "llama3.2")
|
|
prompt = request.args.get("prompt")
|
|
|
|
db = SessionLocal()
|
|
db.add(Message(id=str(uuid.uuid4()), conversation_id=cid, role="user", content=prompt))
|
|
db.commit()
|
|
|
|
def generate():
|
|
url = f"{app.config['OLLAMA_BASE_URL']}/api/generate"
|
|
payload = {"model": model, "prompt": prompt, "stream": True}
|
|
|
|
with requests.post(url, json=payload, stream=True) as r:
|
|
buffer = ""
|
|
for line in r.iter_lines():
|
|
if not line:
|
|
continue
|
|
try:
|
|
data = json.loads(line.decode())
|
|
token = data.get("response", "")
|
|
buffer += token
|
|
# streaming animation: send partial buffer
|
|
yield f"data: {json.dumps({'token': token})}\n\n"
|
|
except:
|
|
continue
|
|
db.add(Message(id=str(uuid.uuid4()), conversation_id=cid, role="assistant", content=buffer))
|
|
db.commit()
|
|
db.close()
|
|
|
|
return Response(generate(), mimetype="text/event-stream")
|
|
|
|
|
|
@app.route("/api/share/<cid>", methods=["POST"])
|
|
@login_required
|
|
@require_capability("can_share")
|
|
def share(cid):
|
|
target_email = request.json.get("email")
|
|
db = SessionLocal()
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
conv = db.query(Conversation).filter_by(id=cid, user_email=email).first()
|
|
if not conv:
|
|
return jsonify({"erro": "Conversa não encontrada."}), 404
|
|
|
|
conv.shared = target_email
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
|
|
@app.route("/api/tags/<cid>", methods=["POST"])
|
|
@login_required
|
|
def update_tags(cid):
|
|
tags = request.json.get("tags", [])
|
|
db = SessionLocal()
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
conv = db.query(Conversation).filter_by(id=cid, user_email=email).first()
|
|
if not conv:
|
|
return jsonify({"erro": "Conversa não encontrada."}), 404
|
|
|
|
conv.tags = ",".join(tags)
|
|
db.commit()
|
|
db.close()
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@app.route("/api/search")
|
|
@login_required
|
|
@rate_limited
|
|
def search():
|
|
q = request.args.get("q", "").lower()
|
|
user_email = session["user"]["email"]
|
|
db = SessionLocal()
|
|
convs = db.query(Conversation).filter_by(user_email=user_email).all()
|
|
results = []
|
|
for c in convs:
|
|
for m in c.messages:
|
|
if q in m.content.lower():
|
|
results.append({
|
|
"conversation_id": c.id,
|
|
"title": c.title,
|
|
"role": m.role,
|
|
"content": m.content,
|
|
"created_at": m.created_at.isoformat()
|
|
})
|
|
db.close()
|
|
return jsonify(results)
|
|
|
|
from pywhispercpp.model import Model
|
|
@app.route("/api/audio/transcribe/<cid>", methods=["POST"])
|
|
@login_required
|
|
@require_capability("can_use_audio")
|
|
def transcribe(cid):
|
|
# Initialize the model (automatically downloads 'base.en' if not present)
|
|
model = Model('base.en', print_realtime=False, print_progress=False)
|
|
|
|
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"error": "No audio file"}), 400
|
|
|
|
path = f"/data/audio-{uuid.uuid4()}.wav"
|
|
file.save(path)
|
|
|
|
# Example using whisper (pseudo-code)
|
|
# result = whisper.transcribe(path)
|
|
# text = result["text"]
|
|
|
|
# Transcribe your audio file (must be 16kHz WAV format)
|
|
segments = model.transcribe(path)
|
|
|
|
# Print the text results
|
|
for segment in segments:
|
|
print(f"[{segment.t0} -> {segment.t1}]: {segment.text}")
|
|
|
|
text = "Transcribed text placeholder"
|
|
|
|
db = SessionLocal()
|
|
msg = Message(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
role="user",
|
|
content=text,
|
|
)
|
|
db.add(msg)
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok", "text": text})
|
|
|
|
from gtts import gTTS
|
|
|
|
@app.route("/api/audio/synthesize/<cid>", methods=["POST"])
|
|
@login_required
|
|
@require_capability("can_use_audio")
|
|
def synthesize(cid):
|
|
text = request.json.get("text", "")
|
|
if not text:
|
|
return jsonify({"error": "No text"}), 400
|
|
|
|
tts = gTTS(text=text, lang="en")
|
|
path = f"/data/tts-{uuid.uuid4()}.mp3"
|
|
tts.save(path)
|
|
|
|
return send_file(path, mimetype="audio/mpeg", as_attachment=False)
|
|
|
|
|
|
from PIL import Image
|
|
|
|
@app.route("/api/vision/<cid>", methods=["POST"])
|
|
@login_required
|
|
@require_capability("can_use_vision")
|
|
def vision(cid):
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"error": "No image"}), 400
|
|
|
|
img_id = str(uuid.uuid4())
|
|
path = f"/data/img-{img_id}.png"
|
|
file.save(path)
|
|
|
|
# Here you would call a vision-capable model (pseudo-code).
|
|
# description = call_vision_model(path)
|
|
|
|
description = "Image description placeholder"
|
|
|
|
db = SessionLocal()
|
|
msg = Message(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
role="assistant",
|
|
content=description,
|
|
)
|
|
db.add(msg)
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok", "description": description})
|
|
|
|
|
|
@app.route("/api/subscribe", methods=["POST"])
|
|
@login_required
|
|
def subscribe():
|
|
data = request.json
|
|
plan_id = data["plan_id"]
|
|
|
|
db = SessionLocal()
|
|
|
|
if app.config["AUTH_MODE"] == "oauth":
|
|
email = session["user"]["email"]
|
|
else:
|
|
email = session["db_user"]["email"]
|
|
|
|
sub = Subscription(
|
|
id=str(uuid.uuid4()),
|
|
user_email=email,
|
|
plan_id=plan_id,
|
|
status="active",
|
|
renew_at=datetime.utcnow() + timedelta(days=30)
|
|
)
|
|
|
|
user = db.query(User).filter_by(email=email).first()
|
|
user.plan_id = plan_id
|
|
|
|
db.add(sub)
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
# ===========
|
|
# IO
|
|
|
|
|
|
from io.document import convert_document
|
|
|
|
@app.route("/api/import/d/<cid>", methods=["POST"])
|
|
@login_required
|
|
def import_document(cid):
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"erro": "Nenhum arquivo enviado."}), 400
|
|
|
|
ext = file.filename.lower().split(".")[-1]
|
|
if ext not in ["doc", "docx", "odt", "txt", "md", "html", "htm"]:
|
|
return jsonify({"erro": "Formato não suportado."}), 400
|
|
|
|
email = session["db_user"]["email"] if app.config["AUTH_MODE"] == "db" else session["user"]["email"]
|
|
|
|
temp_path = f"/data/{uuid.uuid4()}.{ext}"
|
|
file.save(temp_path)
|
|
|
|
text = convert_document(temp_path)
|
|
|
|
# Convert to TSV (single column)
|
|
tsv = "\x09".join([normalize_newlines(text)])
|
|
filename = f"{file.filename}.tsv"
|
|
|
|
file_id = str(uuid.uuid4())
|
|
path = f"/data/{file_id}_{filename}"
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(tsv)
|
|
|
|
db = SessionLocal()
|
|
db.add(File(
|
|
id=file_id,
|
|
user_email=email,
|
|
path=path,
|
|
original_name=filename
|
|
))
|
|
|
|
msg = Message(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
role="user",
|
|
content=f"Arquivo importado: {filename}\n```\n{tsv}\n```"
|
|
)
|
|
db.add(msg)
|
|
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok", "filename": filename})
|
|
|
|
@app.route("/api/export/txt/<cid>/<filename>")
|
|
@login_required
|
|
def export_txt(cid, filename):
|
|
email = session["db_user"]["email"] if app.config["AUTH_MODE"] == "db" else session["user"]["email"]
|
|
|
|
db = SessionLocal()
|
|
msgs = (
|
|
db.query(Message)
|
|
.join(Conversation)
|
|
.filter(Conversation.id == cid, Conversation.user_email == email)
|
|
.order_by(Message.created_at)
|
|
.all()
|
|
)
|
|
db.close()
|
|
|
|
text = "\n".join([m.content for m in msgs])
|
|
out = f"/data/{uuid.uuid4()}_{filename}"
|
|
|
|
with open(out, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
|
|
return send_file(out, as_attachment=True, download_name=filename)
|
|
|
|
@app.route("/api/export/md/<cid>/<filename>")
|
|
@login_required
|
|
def export_md(cid, filename):
|
|
# similar to TXT, but wrap content in Markdown
|
|
...
|
|
|
|
@app.route("/api/export/html/<cid>/<filename>")
|
|
@login_required
|
|
def export_html(cid, filename):
|
|
# wrap messages in <p> tags
|
|
...
|
|
|
|
from docx import Document
|
|
|
|
@app.route("/api/export/docx/<cid>/<filename>")
|
|
@login_required
|
|
def export_docx(cid, filename):
|
|
email = session["db_user"]["email"] if app.config["AUTH_MODE"] == "db" else session["user"]["email"]
|
|
|
|
db = SessionLocal()
|
|
msgs = (
|
|
db.query(Message)
|
|
.join(Conversation)
|
|
.filter(Conversation.id == cid, Conversation.user_email == email)
|
|
.order_by(Message.created_at)
|
|
.all()
|
|
)
|
|
db.close()
|
|
|
|
doc = Document()
|
|
for m in msgs:
|
|
doc.add_paragraph(m.content)
|
|
|
|
out = f"/data/{uuid.uuid4()}_{filename}"
|
|
doc.save(out)
|
|
|
|
return send_file(out, as_attachment=True, download_name=filename)
|
|
|
|
@app.route("/api/export/odt/<cid>/<filename>")
|
|
@login_required
|
|
def export_odt(cid, filename):
|
|
# similar approach using odfpy
|
|
...
|
|
|
|
|
|
|
|
from io.spreadsheet import convert_excel, convert_excel_all_sheets
|
|
|
|
@app.route("/api/import/s/<cid>", methods=["POST"])
|
|
@login_required
|
|
def import_excel(cid):
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"error": "No file"}), 400
|
|
|
|
ext = file.filename.lower().split(".")[-1]
|
|
if ext not in ["xlsx", "xls", "ods"]:
|
|
return jsonify({"error": "Unsupported format"}), 400
|
|
|
|
temp_path = f"/data/{uuid.uuid4()}.{ext}"
|
|
file.save(temp_path)
|
|
|
|
# tsv = convert_excel(temp_path)
|
|
# Convert all sheets
|
|
sheets = convert_excel_all_sheets(temp_path)
|
|
|
|
|
|
db = SessionLocal()
|
|
# Each sheet becomes a separate TSV message
|
|
for sheet_name, tsv in sheets.items():
|
|
msg = Message(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
role="user",
|
|
content=f"Arquivo importado: {sheet_name}.tsv\n```\n{tsv}\n```"
|
|
)
|
|
db.add(msg)
|
|
|
|
# return jsonify({"status": "ok"})
|
|
return jsonify({"status": "ok", "sheets": list(sheets.keys())})
|
|
|
|
|
|
from io.presentation import convert_presentation
|
|
|
|
@app.route("/api/import/p/<cid>", methods=["POST"])
|
|
@login_required
|
|
def import_presentation(cid):
|
|
file = request.files.get("file")
|
|
if not file:
|
|
return jsonify({"erro": "Nenhum arquivo enviado."}), 400
|
|
|
|
ext = file.filename.lower().split(".")[-1]
|
|
if ext not in ["ppt", "pptx", "odp"]:
|
|
return jsonify({"erro": "Formato não suportado."}), 400
|
|
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
|
|
temp_path = f"/data/{uuid.uuid4()}.{ext}"
|
|
file.save(temp_path)
|
|
|
|
slides = convert_presentation(temp_path)
|
|
|
|
db = SessionLocal()
|
|
|
|
for slide_name, content in slides:
|
|
tsv = "\x09".join([normalize_newlines(content)])
|
|
filename = f"{slide_name}.tsv"
|
|
|
|
file_id = str(uuid.uuid4())
|
|
path = f"/data/{file_id}_{filename}"
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(tsv)
|
|
|
|
db.add(File(
|
|
id=file_id,
|
|
user_email=email,
|
|
path=path,
|
|
original_name=filename
|
|
))
|
|
|
|
msg = Message(
|
|
id=str(uuid.uuid4()),
|
|
conversation_id=cid,
|
|
role="user",
|
|
content=f"Slide importado: {filename}\n```\n{tsv}\n```"
|
|
)
|
|
db.add(msg)
|
|
|
|
db.commit()
|
|
db.close()
|
|
|
|
return jsonify({"status": "ok", "slides": [s[0] for s in slides]})
|
|
|
|
from pptx import Presentation
|
|
|
|
@app.route("/api/export/pptx/<cid>/<filename>")
|
|
@login_required
|
|
def export_pptx(cid, filename):
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
|
|
db = SessionLocal()
|
|
msgs = (
|
|
db.query(Message)
|
|
.join(Conversation)
|
|
.filter(Conversation.id == cid, Conversation.user_email == email)
|
|
.order_by(Message.created_at)
|
|
.all()
|
|
)
|
|
db.close()
|
|
|
|
prs = Presentation()
|
|
for m in msgs:
|
|
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
|
body = slide.shapes.placeholders[1]
|
|
tf = body.text_frame
|
|
tf.text = m.content
|
|
|
|
out = f"/data/{uuid.uuid4()}_{filename}"
|
|
prs.save(out)
|
|
|
|
return send_file(out, as_attachment=True, download_name=filename)
|
|
|
|
@app.route("/api/export/odp/<cid>/<filename>")
|
|
@login_required
|
|
def export_odp(cid, filename):
|
|
# similar approach using odfpy
|
|
# create ODP with each message as a slide
|
|
...
|
|
|
|
|
|
|
|
# ================
|
|
# TASKS
|
|
# ================
|
|
from tasks import generate_task
|
|
|
|
@app.route("/api/queue/generate", methods=["POST"])
|
|
@login_required
|
|
def queue_generate():
|
|
data = request.json
|
|
cid = data["conversation_id"]
|
|
model = data["model"]
|
|
prompt = data["prompt"]
|
|
|
|
task = generate_task.delay(cid, model, prompt)
|
|
|
|
return jsonify({"task_id": task.id})
|
|
|
|
@app.route("/api/generate", methods=["POST"])
|
|
@login_required
|
|
def generate():
|
|
data = request.json
|
|
cid = data["conversation_id"]
|
|
model = data["model"]
|
|
prompt = data["prompt"]
|
|
|
|
db = SessionLocal()
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
user = db.query(User).filter_by(email=email).first()
|
|
db.close()
|
|
|
|
#task = enqueue_by_priority(user, cid, model, prompt)
|
|
task = enqueue_by_priority(user.email, cid, model, prompt)
|
|
|
|
return jsonify({"task_id": task.id})
|
|
|
|
|
|
|
|
|
|
|
|
from celery_app import celery
|
|
|
|
@app.route("/api/queue/status/<task_id>")
|
|
@login_required
|
|
def queue_status(task_id):
|
|
result = celery.AsyncResult(task_id)
|
|
return jsonify({
|
|
"task_id": task_id,
|
|
"state": result.state,
|
|
"result": result.result if result.ready() else None
|
|
})
|
|
|
|
|
|
|
|
def enqueue_by_priority(user, cid, model, prompt):
|
|
if user.priority == "system_admin":
|
|
return system_admin_generate.delay(cid, model, prompt)
|
|
|
|
if user.priority == "corp_admin":
|
|
return corp_admin_generate.delay(cid, model, prompt)
|
|
|
|
return user_generate.delay(cid, model, prompt)
|
|
|
|
|
|
def enqueue_task(model, prompt, cid, user):
|
|
db = SessionLocal()
|
|
plan = db.query(Plan).filter_by(id=user.plan_id).first()
|
|
db.close()
|
|
|
|
queue = plan.priority_queue # default, premium, admin
|
|
|
|
if queue == "admin":
|
|
return admin_generate.delay(cid, model, prompt)
|
|
if queue == "premium":
|
|
return premium_generate.delay(cid, model, prompt)
|
|
return generate_task.delay(cid, model, prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ***
|
|
|
|
import json
|
|
import requests
|
|
from flask import Flask, Response, request, jsonify, session, redirect, url_for
|
|
from authlib.integrations.flask_client import OAuth
|
|
from functools import wraps
|
|
|
|
from config import Config
|
|
from chat_store import ChatStore
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
app.secret_key = app.config["SECRET_KEY"]
|
|
|
|
store = ChatStore()
|
|
|
|
oauth = OAuth(app)
|
|
google = oauth.register(
|
|
name="google",
|
|
client_id=app.config["GOOGLE_CLIENT_ID"],
|
|
client_secret=app.config["GOOGLE_CLIENT_SECRET"],
|
|
access_token_url="https://oauth2.googleapis.com/token",
|
|
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
|
api_base_url="https://www.googleapis.com/oauth2/v2/",
|
|
client_kwargs={"scope": "openid email profile"},
|
|
)
|
|
|
|
def login_required(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
if "user" not in session:
|
|
return redirect(url_for("login"))
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
@app.route("/login")
|
|
def login():
|
|
return google.authorize_redirect(app.config["GOOGLE_REDIRECT_URI"])
|
|
|
|
@app.route("/oauth/callback")
|
|
def oauth_callback():
|
|
token = google.authorize_access_token()
|
|
user_info = google.get("userinfo").json()
|
|
session["user"] = user_info
|
|
return redirect("/")
|
|
|
|
@app.route("/api/new_conversation")
|
|
@login_required
|
|
def new_conversation():
|
|
cid = store.new_conversation()
|
|
return jsonify({"conversation_id": cid})
|
|
|
|
@app.route("/api/history/<cid>")
|
|
@login_required
|
|
def history(cid):
|
|
return jsonify(store.get_messages(cid))
|
|
|
|
@app.route("/api/stream")
|
|
@login_required
|
|
def stream():
|
|
cid = request.args.get("conversation_id")
|
|
model = request.args.get("model", "llama3.2")
|
|
prompt = request.args.get("prompt")
|
|
|
|
store.add_message(cid, "user", prompt)
|
|
|
|
def generate():
|
|
url = f"{app.config['OLLAMA_BASE_URL']}/api/generate"
|
|
payload = {"model": model, "prompt": prompt, "stream": True}
|
|
|
|
with requests.post(url, json=payload, stream=True) as r:
|
|
for line in r.iter_lines():
|
|
if not line:
|
|
continue
|
|
try:
|
|
data = json.loads(line.decode())
|
|
token = data.get("response", "")
|
|
store.add_message(cid, "assistant", token)
|
|
yield f"data: {json.dumps({'token': token})}\n\n"
|
|
except:
|
|
continue
|
|
|
|
return Response(generate(), mimetype="text/event-stream")
|
|
|
|
|
|
# ***
|
|
|
|
import os
|
|
import json
|
|
import requests
|
|
from functools import wraps
|
|
|
|
from flask import Flask, request, jsonify, redirect, session, url_for
|
|
from authlib.integrations.flask_client import OAuth
|
|
|
|
from config import Config
|
|
|
|
# All comments are in English.
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
app.secret_key = app.config["SECRET_KEY"]
|
|
|
|
oauth = OAuth(app)
|
|
google = oauth.register(
|
|
name="google",
|
|
client_id=app.config["GOOGLE_CLIENT_ID"],
|
|
client_secret=app.config["GOOGLE_CLIENT_SECRET"],
|
|
access_token_url="https://oauth2.googleapis.com/token",
|
|
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
|
authorize_params={"access_type": "offline", "prompt": "consent"},
|
|
api_base_url="https://www.googleapis.com/oauth2/v2/",
|
|
client_kwargs={"scope": "openid email profile"},
|
|
)
|
|
|
|
|
|
def login_required(f):
|
|
"""Decorator to ensure the user is authenticated via Google OAuth."""
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
if "user" not in session:
|
|
return redirect(url_for("login"))
|
|
return f(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
@app.route("/login")
|
|
def login():
|
|
"""Start Google OAuth login flow."""
|
|
return google.authorize_redirect(app.config["Google_REDIRECT_URI"])
|
|
|
|
|
|
@app.route("/oauth/callback")
|
|
def oauth_callback():
|
|
"""Handle Google OAuth callback and store user session."""
|
|
token = google.authorize_access_token()
|
|
user_info = google.get("userinfo").json()
|
|
|
|
session["user"] = {
|
|
"email": user_info.get("email"),
|
|
"name": user_info.get("name"),
|
|
}
|
|
|
|
return redirect(url_for("home"))
|
|
|
|
|
|
@app.route("/")
|
|
def home():
|
|
return jsonify({
|
|
"message": "Ollama Gateway is running.",
|
|
"domain": "https://safira.renatorosa.com",
|
|
"auth": "Google OAuth required for /api/* endpoints.",
|
|
"endpoints": {
|
|
"health": "/health",
|
|
"generate": "/api/generate"
|
|
}
|
|
})
|
|
|
|
|
|
@app.route("/health")
|
|
def health():
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@app.route("/api/generate", methods=["POST"])
|
|
@login_required
|
|
def generate():
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
model_name = data.get("model", "llama3.2")
|
|
prompt = data.get("prompt")
|
|
|
|
if not prompt:
|
|
return jsonify({"error": "Missing 'prompt' field."}), 400
|
|
|
|
model_id = app.config["MODEL_MAP"].get(model_name)
|
|
if not model_id:
|
|
return jsonify({"error": f"Unsupported model '{model_name}'."}), 400
|
|
|
|
# ollama_url = f"{app.config['OLLAMA_BASE_URL']}/api/generate"
|
|
# payload = {"model": model_id, "prompt": prompt}
|
|
# try:
|
|
# response = requests.post(ollama_url, json=payload, timeout=60)
|
|
# response.raise_for_status()
|
|
# except requests.RequestException as e:
|
|
# return jsonify({"error": "Failed to call Ollama API.", "details": str(e)}), 502
|
|
# try:
|
|
# ollama_data = response.json()
|
|
# except json.JSONDecodeError:
|
|
# return jsonify({"error": "Invalid JSON response from Ollama."}), 502
|
|
#output_text = ollama_data.get("response") or ollama_data.get("output") or ""
|
|
#return jsonify({"model": model_name, "output": output_text})
|
|
email = session["user"]["email"] if app.config["AUTH_MODE"] == "oauth" else session["db_user"]["email"]
|
|
|
|
cached = get_cache(email, model, prompt)
|
|
|
|
|
|
if cached:
|
|
return jsonify({"model": model, "output": cached, "cached": True})
|
|
|
|
result, used_model = call_ollama_with_fallback({"prompt": prompt})
|
|
output = result.get("response")
|
|
|
|
set_cache(email, used_model, prompt, output)
|
|
|
|
|
|
return jsonify({"model": used_model, "output": output, "cached": False})
|
|
|
|
@app.route("/api/multi", methods=["POST"])
|
|
@login_required
|
|
@rate_limited
|
|
def multi():
|
|
data = request.json
|
|
models = data.get("models", ["llama3.2", "gemma4"])
|
|
prompt = data.get("prompt")
|
|
|
|
results = {}
|
|
|
|
for m in models:
|
|
try:
|
|
r = requests.post(
|
|
f"{app.config['OLLAMA_BASE_URL']}/api/generate",
|
|
json={"model": m, "prompt": prompt}
|
|
)
|
|
r.raise_for_status()
|
|
results[m] = r.json().get("response")
|
|
except Exception as e:
|
|
results[m] = f"Error: {str(e)}"
|
|
|
|
return jsonify(results)
|
|
|
|
@app.route("/api/multi/stream")
|
|
@login_required
|
|
def multi_stream():
|
|
models = request.args.get("models", "llama3.2,gemma4").split(",")
|
|
prompt = request.args.get("prompt")
|
|
|
|
def generate():
|
|
for m in models:
|
|
yield f"event: model\n"
|
|
yield f"data: {json.dumps({'model': m})}\n\n"
|
|
|
|
url = f"{app.config['OLLAMA_BASE_URL']}/api/generate"
|
|
payload = {"model": m, "prompt": prompt, "stream": True}
|
|
|
|
with requests.post(url, json=payload, stream=True) as r:
|
|
for line in r.iter_lines():
|
|
if not line:
|
|
continue
|
|
data = json.loads(line.decode())
|
|
token = data.get("response", "")
|
|
yield f"event: token\n"
|
|
yield f"data: {json.dumps({'model': m, 'token': token})}\n\n"
|
|
|
|
return Response(generate(), mimetype="text/event-stream")
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8000)
|
|
|
|
# ***
|
|
|
|
import os
|
|
import json
|
|
import requests
|
|
from functools import wraps
|
|
|
|
from flask import Flask, request, jsonify, redirect, session, url_for
|
|
from authlib.integrations.flask_client import OAuth
|
|
|
|
from config import Config
|
|
|
|
# All comments are in English.
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
|
|
# Configure session secret key.
|
|
app.secret_key = app.config["SECRET_KEY"]
|
|
|
|
# Configure OAuth with Google.
|
|
oauth = OAuth(app)
|
|
google = oauth.register(
|
|
name="google",
|
|
client_id=app.config["GOOGLE_CLIENT_ID"],
|
|
client_secret=app.config["GOOGLE_CLIENT_SECRET"],
|
|
access_token_url="https://oauth2.googleapis.com/token",
|
|
access_token_params=None,
|
|
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
|
authorize_params={
|
|
"access_type": "offline",
|
|
"prompt": "consent",
|
|
},
|
|
api_base_url="https://www.googleapis.com/oauth2/v2/",
|
|
client_kwargs={"scope": "openid email profile"},
|
|
)
|
|
|
|
|
|
def login_required(f):
|
|
"""Decorator to ensure the user is authenticated via Google OAuth."""
|
|
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
# Check if user info is stored in session.
|
|
if "user" not in session:
|
|
# Redirect to login if not authenticated.
|
|
return redirect(url_for("login"))
|
|
return f(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
@app.route("/login")
|
|
def login():
|
|
"""Start Google OAuth login flow."""
|
|
redirect_uri = app.config["GOOGLE_REDIRECT_URI"]
|
|
# Use the configured redirect URI for OAuth callback.
|
|
return google.authorize_redirect(redirect_uri)
|
|
|
|
|
|
@app.route("/oauth/callback")
|
|
def oauth_callback():
|
|
"""Handle Google OAuth callback and store user session."""
|
|
# Exchange authorization code for tokens.
|
|
token = google.authorize_access_token()
|
|
# Fetch user info from Google.
|
|
resp = google.get("userinfo")
|
|
user_info = resp.json()
|
|
|
|
# Store minimal user info in session.
|
|
session["user"] = {
|
|
"email": user_info.get("email"),
|
|
"name": user_info.get("name"),
|
|
}
|
|
|
|
# Redirect to a simple home or API docs page.
|
|
return redirect(url_for("home"))
|
|
|
|
|
|
@app.route("/")
|
|
def home():
|
|
"""Simple home endpoint explaining the service."""
|
|
# This endpoint is intentionally simple and easy to understand.
|
|
return jsonify(
|
|
{
|
|
"message": "Ollama Gateway is running.",
|
|
"auth": "Google OAuth required for /api/* endpoints.",
|
|
"endpoints": {
|
|
"health": "/health",
|
|
"generate": "/api/generate",
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
@app.route("/health")
|
|
def health():
|
|
"""Health check endpoint."""
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@app.route("/api/generate", methods=["POST"])
|
|
@login_required
|
|
def generate():
|
|
"""Generate text using a selected Ollama model.
|
|
|
|
Expected JSON body:
|
|
{
|
|
"model": "llama3.2",
|
|
"prompt": "Your prompt here"
|
|
}
|
|
"""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
# Extract model and prompt from request.
|
|
model_name = data.get("model", "llama3.2")
|
|
prompt = data.get("prompt")
|
|
|
|
if not prompt:
|
|
return jsonify({"error": "Missing 'prompt' field."}), 400
|
|
|
|
# Map user-facing model name to Ollama model identifier.
|
|
model_id = app.config["MODEL_MAP"].get(model_name)
|
|
if not model_id:
|
|
return jsonify({"error": f"Unsupported model '{model_name}'."}), 400
|
|
|
|
# Prepare request to Ollama API.
|
|
ollama_url = f"{app.config['OLLAMA_BASE_URL']}/api/generate"
|
|
payload = {
|
|
"model": model_id,
|
|
"prompt": prompt,
|
|
# You can add more Ollama-specific parameters here if needed.
|
|
}
|
|
|
|
try:
|
|
# Call Ollama HTTP API.
|
|
response = requests.post(ollama_url, json=payload, timeout=60)
|
|
response.raise_for_status()
|
|
except requests.RequestException as e:
|
|
# Handle network or API errors gracefully.
|
|
return jsonify({"error": "Failed to call Ollama API.", "details": str(e)}), 502
|
|
|
|
# Parse Ollama response.
|
|
try:
|
|
ollama_data = response.json()
|
|
except json.JSONDecodeError:
|
|
return jsonify({"error": "Invalid JSON response from Ollama."}), 502
|
|
|
|
# Extract output text (this depends on Ollama's response format).
|
|
output_text = ollama_data.get("response") or ollama_data.get("output") or ""
|
|
|
|
return jsonify(
|
|
{
|
|
"model": model_name,
|
|
"output": output_text,
|
|
}
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# This block is mainly for local debugging.
|
|
# In production, Gunicorn will run the app.
|
|
app.run(host="0.0.0.0", port=8000)
|
|
|
|
|
|
# ***
|
|
|
|
|
|
from flask import Flask, request, jsonify
|
|
from flask_cors import CORS
|
|
from db import Base, engine
|
|
from models import User
|
|
from services.intent_service import IntentService
|
|
from services.mode_service import ModeService
|
|
from services.preference_service import PreferenceService
|
|
from services.suggestion_service import SuggestionService
|
|
from services.document_service import DocumentService
|
|
from services.search_service import SearchService
|
|
from services.monitoring_service import MonitoringService
|
|
from services.export_service import ExportService
|
|
from services.audit_service import AuditService
|
|
from services.security_service import SecurityService
|
|
|
|
from services.ollama_service import OllamaService
|
|
ollama_service = OllamaService()
|
|
|
|
from services.model_router import ModelRouter
|
|
router = ModelRouter()
|
|
generated = router.generate(intent, mode, prompt)
|
|
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
intent_service = IntentService()
|
|
mode_service = ModeService()
|
|
preference_service = PreferenceService()
|
|
suggestion_service = SuggestionService()
|
|
document_service = DocumentService()
|
|
search_service = SearchService()
|
|
monitoring_service = MonitoringService()
|
|
export_service = ExportService()
|
|
audit_service = AuditService()
|
|
security_service = SecurityService()
|
|
|
|
USER_ID = 1
|
|
|
|
@app.route("/api/conversation", methods=["POST"])
|
|
def conversation():
|
|
data = request.json
|
|
message = data.get("message", "")
|
|
|
|
mode = mode_service.get_current_mode(USER_ID)
|
|
if not mode:
|
|
reply = (
|
|
"{user.name}, como você quer trabalhar hoje?\n\n"
|
|
"- Modo Advogado\n- Modo Corporativo\n- Apenas Conversar\n\n"
|
|
"Me diga qual modo você prefere agora."
|
|
)
|
|
return jsonify({"reply": reply, "suggestions": [], "quick_actions": []})
|
|
|
|
intent = intent_service.detect_intent(message)
|
|
prefs = preference_service.get_preferences(USER_ID)
|
|
|
|
if intent == "set_mode":
|
|
mode_service.set_mode(USER_ID, intent_service.extract_mode(message))
|
|
reply = f"Perfeito, vou atuar no modo {mode_service.get_current_mode(USER_ID)}. O que vamos fazer agora?"
|
|
return jsonify({"reply": reply, "suggestions": [], "quick_actions": []})
|
|
|
|
if intent == "document":
|
|
response = document_service.handle(message, mode, prefs)
|
|
elif intent == "search":
|
|
response = search_service.handle(message, mode, prefs)
|
|
elif intent == "monitoring":
|
|
response = monitoring_service.handle(message, mode, prefs)
|
|
else:
|
|
response = document_service.chat_like(message, mode, prefs)
|
|
|
|
suggestions = suggestion_service.generate_suggestions(response, intent, mode, prefs)
|
|
quick_actions = suggestion_service.generate_quick_actions(response, intent, mode, prefs)
|
|
|
|
return jsonify({
|
|
"reply": response["text"],
|
|
"suggestions": suggestions,
|
|
"quick_actions": quick_actions
|
|
})
|
|
|
|
if __name__ == "__main__":
|
|
app.run(debug=True)
|