Files
devops-portal/frontend/ui.py
2026-04-08 00:52:57 +03:00

237 lines
9.3 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 os
from datetime import datetime, timezone
import requests
import streamlit as st
st.set_page_config(page_title="DevOps AI Assistant", page_icon="🚀")
st.title("DevOps AI Assistant")
st.markdown("Портал с данными и инструментами для АС/ФП.")
def _env(name: str, default: str) -> str:
value = os.getenv(name)
return value if value else default
default_backend_base_url = _env("BACKEND_BASE_URL", "http://backend:8000")
def _normalize_base_url(value: str) -> str:
v = (value or "").strip().rstrip("/")
if v.endswith("/generate"):
v = v[: -len("/generate")]
return v.rstrip("/")
with st.sidebar:
st.header("Настройки")
backend_base_url = _normalize_base_url(st.text_input("Backend URL", default_backend_base_url))
model = st.selectbox(
"LLM Модель",
["deepseek-r1:1.5b", "qwen2.5-coder:1.5b", "llama3.1:8b", "Arena Model"],
)
platform = st.radio("Платформа", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"])
if "history" not in st.session_state:
st.session_state.history = []
if "token" not in st.session_state:
st.session_state.token = None
if "asfp_info" not in st.session_state:
st.session_state.asfp_info = {
"name": "",
"owner": "",
"contacts": "",
"description": "",
}
def _push_history(*, platform: str, model: str, prompt: str, status: str, config: str, error: str | None):
st.session_state.history.insert(
0,
{
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
"platform": platform,
"model": model,
"prompt": prompt,
"status": status,
"error": error,
"config": config,
},
)
st.session_state.history = st.session_state.history[:50]
def _auth_headers():
if not st.session_state.token:
return {}
return {"Authorization": f"Bearer {st.session_state.token}"}
def _detail_or_text(resp: requests.Response) -> str:
try:
payload = resp.json()
if isinstance(payload, dict) and payload.get("detail"):
return str(payload["detail"])
except Exception:
pass
return resp.text
def _show_account():
st.subheader("Аутентификация")
c1, c2 = st.columns(2)
with c1:
st.markdown("**Регистрация**")
reg_email = st.text_input("Email", key="reg_email")
reg_full_name = st.text_input("Имя (опционально)", key="reg_full_name")
reg_password = st.text_input("Пароль", type="password", key="reg_password")
if st.button("Создать аккаунт", key="btn_register"):
try:
r = requests.post(
f"{backend_base_url}/auth/register",
json={"email": reg_email, "password": reg_password, "full_name": reg_full_name or None},
timeout=30,
)
if r.status_code >= 400:
st.error(f"Ошибка регистрации: {_detail_or_text(r)}")
else:
st.success("Аккаунт создан. Теперь можно войти.")
except Exception as e:
st.error(f"Ошибка подключения к бекенду: {e}")
with c2:
st.markdown("**Вход**")
login_email = st.text_input("Email ", key="login_email")
login_password = st.text_input("Пароль ", type="password", key="login_password")
login_col_a, login_col_b = st.columns([1, 1])
with login_col_a:
if st.button("Войти", type="primary", key="btn_login"):
try:
r = requests.post(
f"{backend_base_url}/auth/login",
json={"email": login_email, "password": login_password},
timeout=30,
)
if r.status_code >= 400:
st.error(f"Ошибка входа: {_detail_or_text(r)}")
else:
st.session_state.token = r.json().get("access_token")
st.success("Успешный вход.")
st.rerun()
except Exception as e:
st.error(f"Ошибка подключения к бекенду: {e}")
with login_col_b:
if st.button("Выйти", disabled=not bool(st.session_state.token), key="btn_logout"):
st.session_state.token = None
st.rerun()
st.divider()
if st.session_state.token:
try:
r = requests.get(f"{backend_base_url}/auth/me", headers=_auth_headers(), timeout=20)
if r.status_code >= 400:
st.session_state.token = None
st.warning("Сессия истекла или токен неверный. Войдите заново.")
st.rerun()
else:
st.json(r.json())
except Exception as e:
st.error(f"Ошибка подключения к бекенду: {e}")
# Auth wall: пока пользователь не вошёл — показываем только экран входа/регистрации.
if not st.session_state.token:
_show_account()
st.stop()
tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
[
"Сведения об АС/ФП",
"Автономные внедрения",
"Инфраструктура АС/ФП",
"Инструменты",
"Новости",
]
)
with tab_info:
st.subheader("Сведения об АС/ФП")
st.caption("Черновик формы. Дальше можно привязать к Postgres и хранить как сущность.")
st.session_state.asfp_info["name"] = st.text_input("Название", st.session_state.asfp_info["name"])
st.session_state.asfp_info["owner"] = st.text_input("Владелец", st.session_state.asfp_info["owner"])
st.session_state.asfp_info["contacts"] = st.text_input("Контакты", st.session_state.asfp_info["contacts"])
st.session_state.asfp_info["description"] = st.text_area(
"Описание", st.session_state.asfp_info["description"], height=120
)
st.divider()
st.json(st.session_state.asfp_info)
with tab_autonomous:
st.subheader("Автономные внедрения")
st.info("Сюда можно добавить список внедрений, статус, даты, ответственных и ссылки на релизы/тикеты.")
with tab_infra:
st.subheader("Инфраструктура АС/ФП")
st.info("Сюда можно добавить окружения, кластера, сервисы, ресурсы, сетевые зависимости, секреты и т.д.")
with tab_tools:
st.subheader("Инструменты")
st.caption(f"Backend URL: {backend_base_url}")
st.markdown("### Pipeline generator")
user_input = st.text_area(
"Опишите пайплайн (например: 'Python app with pytest and docker build')",
height=150,
key="generator_prompt",
)
col_a, col_b = st.columns([1, 3])
with col_a:
generate_clicked = st.button("Сгенерировать конфигурацию", type="primary")
with col_b:
st.caption("Подсказка: чем конкретнее требования, тем валиднее YAML.")
if generate_clicked:
if user_input.strip():
with st.spinner("LLM думает..."):
try:
res = requests.post(
f"{backend_base_url}/generate",
json={"description": user_input, "platform": platform, "model": model},
headers=_auth_headers(),
timeout=90,
)
res.raise_for_status()
data = res.json()
status = data.get("status", "error")
config = data.get("config", "")
err = data.get("validation_error")
_push_history(
platform=platform,
model=model,
prompt=user_input,
status=status,
config=config,
error=err,
)
if status == "ok":
st.success("Конфигурация валидна.")
else:
st.error(f"Ошибка валидации: {err}")
st.code(config, language="yaml")
st.download_button("Скачать .yml", config, file_name="pipeline.yml")
except Exception as e:
st.error(f"Ошибка подключения к бекенду: {e}")
else:
st.warning("Пожалуйста, введите описание.")
with tab_news:
st.subheader("Новости")
st.info("Сюда можно вывести новости/изменения: релизы, инциденты, регламентные работы, обновления инструментов.")