Files
devops-portal/frontend/ui.py
2026-04-07 23:48:47 +03:00

248 lines
9.7 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
import yaml
st.set_page_config(page_title="DevOps AI Assistant", page_icon="🚀")
st.title("DevOps AI Assistant")
st.markdown("Веб-интерфейс для генерации и проверки CI/CD конфигураций.")
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
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]
tab_overview, tab_account, tab_generator, tab_validator, tab_history = st.tabs(
["Обзор", "Аккаунт", "Pipeline generator", "Проверка YAML", "История"]
)
with tab_overview:
st.subheader("Что умеет сервис")
st.markdown(
"- **Pipeline generator**: генерирует YAML через backend (FastAPI → Ollama).\n"
"- **Проверка YAML**: локально проверяет синтаксис YAML, чтобы быстро править конфиг.\n"
"- **История**: хранит последние результаты в рамках сессии браузера."
)
st.divider()
st.caption(f"Backend URL: {backend_base_url}")
if st.session_state.token:
st.success("Вы вошли в систему.")
else:
st.info("Вы не авторизованы.")
def _auth_headers():
if not st.session_state.token:
return {}
return {"Authorization": f"Bearer {st.session_state.token}"}
with tab_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("Создать аккаунт"):
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"Ошибка регистрации: {r.text}")
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"):
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"Ошибка входа: {r.text}")
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)):
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.error(f"Не удалось получить профиль: {r.text}")
else:
st.json(r.json())
except Exception as e:
st.error(f"Ошибка подключения к бекенду: {e}")
with tab_generator:
st.subheader("AI CI/CD 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",
disabled=not bool(st.session_state.token),
)
with col_b:
if st.session_state.token:
st.caption("Подсказка: чем конкретнее требования, тем валиднее YAML.")
else:
st.warning("Нужно войти во вкладке «Аккаунт», чтобы генерировать.")
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_validator:
st.subheader("Локальная проверка YAML")
yaml_text = st.text_area("Вставьте YAML для проверки", height=240, key="validator_yaml")
c1, c2 = st.columns([1, 1])
with c1:
validate_clicked = st.button("Проверить YAML")
with c2:
if st.button("Вставить последний результат", disabled=(len(st.session_state.history) == 0)):
st.session_state.validator_yaml = (st.session_state.history[0].get("config") or "")
st.rerun()
if validate_clicked:
if yaml_text.strip():
try:
yaml.safe_load(yaml_text)
st.success("YAML синтаксически валиден.")
except Exception as e:
st.error(f"Ошибка YAML: {e}")
else:
st.warning("Вставьте YAML.")
with tab_history:
st.subheader("История (последние 50)")
if not st.session_state.history:
st.info("Пока пусто. Сгенерируйте конфигурацию во вкладке Pipeline generator.")
else:
if st.button("Очистить историю"):
st.session_state.history = []
st.rerun()
for idx, item in enumerate(st.session_state.history):
title = f"[{item['ts']}] {item['platform']} · {item['model']} · {item['status']}"
with st.expander(title, expanded=(idx == 0)):
st.caption(item.get("prompt", ""))
if item.get("error"):
st.error(f"Validation error: {item['error']}")
st.code(item.get("config", ""), language="yaml")
st.download_button(
"Скачать .yml",
item.get("config", ""),
file_name=f"pipeline-{idx+1}.yml",
key=f"download_{idx}",
)