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("Бюда ΠΌΠΎΠΆΠ½ΠΎ вывСсти новости/измСнСния: Ρ€Π΅Π»ΠΈΠ·Ρ‹, ΠΈΠ½Ρ†ΠΈΠ΄Π΅Π½Ρ‚Ρ‹, Ρ€Π΅Π³Π»Π°ΠΌΠ΅Π½Ρ‚Π½Ρ‹Π΅ Ρ€Π°Π±ΠΎΡ‚Ρ‹, обновлСния инструмСнтов.")