From 214c9495f39c743d4643ea5d4f193af0b04300a5 Mon Sep 17 00:00:00 2001 From: Damir Date: Fri, 10 Apr 2026 00:06:55 +0300 Subject: [PATCH] update --- backend/main.py | 52 +++++++++++++++++++++++++++++++++++++++++++- frontend/ui.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index 3a69fc6..937188e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -18,8 +18,16 @@ def _env(name: str, default: str) -> str: return value if value else default -# Example for local Ollama: http://localhost:11434/api/generate +# Example: http://host:11434/api/generate — для чата берётся тот же хост + /api/chat OLLAMA_API_URL = _env("OLLAMA_API_URL", "http://host.docker.internal:11434/api/generate") + + +def _ollama_chat_url() -> str: + base = OLLAMA_API_URL.replace("/api/generate", "").rstrip("/") + return f"{base}/api/chat" + + +OLLAMA_CHAT_URL = _env("OLLAMA_CHAT_URL", _ollama_chat_url()) REQUIRE_AUTH_FOR_GENERATE = _env("REQUIRE_AUTH_FOR_GENERATE", "true").lower() in ("1", "true", "yes", "on") from backend.auth import ( # noqa: E402 @@ -40,6 +48,16 @@ class GenerationRequest(BaseModel): model: str = "codellama" +class ChatMessageIn(BaseModel): + role: str + content: str + + +class ChatRequest(BaseModel): + model: str + messages: list[ChatMessageIn] + + def validate_config(config_text: str): try: yaml.safe_load(config_text) @@ -151,6 +169,38 @@ def me(user: User = Depends(get_current_user)): return {"id": str(user.id), "email": user.email, "full_name": user.full_name, "is_admin": user.is_admin} +@app.post("/chat") +def chat_with_ollama( + req: ChatRequest, + user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None, +): + if not req.messages: + raise HTTPException(status_code=400, detail="messages must not be empty") + for m in req.messages: + if m.role not in ("user", "assistant", "system"): + raise HTTPException(status_code=400, detail=f"invalid role: {m.role}") + + payload = { + "model": req.model, + "messages": [{"role": m.role, "content": m.content} for m in req.messages], + "stream": False, + } + try: + response = requests.post(OLLAMA_CHAT_URL, json=payload, timeout=120) + response.raise_for_status() + data = response.json() + msg = data.get("message") or {} + content = msg.get("content", "") + if not content and isinstance(data.get("response"), str): + content = data["response"] + return {"role": "assistant", "content": content} + except requests.HTTPError as e: + detail = e.response.text if e.response is not None else str(e) + raise HTTPException(status_code=502, detail=f"Ollama HTTP error: {detail}") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Ollama error: {str(e)}") + + @app.post("/generate") async def generate_pipeline( req: GenerationRequest, diff --git a/frontend/ui.py b/frontend/ui.py index d172d59..8952f12 100644 --- a/frontend/ui.py +++ b/frontend/ui.py @@ -64,7 +64,7 @@ with st.sidebar: ) platform = st.radio("Платформа", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"]) st.divider() - st.caption("Портал АС/ФП · конфигурации в разделе «Инструменты»") + st.caption("Портал АС/ФП · чат и генератор используют выбранную LLM модель.") if "history" not in st.session_state: st.session_state.history = [] @@ -77,6 +77,8 @@ if "asfp_info" not in st.session_state: "contacts": "", "description": "", } +if "chat_messages" not in st.session_state: + st.session_state.chat_messages = [] def _push_history(*, platform: str, model: str, prompt: str, status: str, config: str, error: str | None): @@ -182,8 +184,9 @@ if not st.session_state.token: _title_bar(show_logout=True) st.caption("Портал с данными и инструментами для АС/ФП.") -tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs( +tab_chat, tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs( [ + "Чат с ИИ", "Сведения об АС/ФП", "Автономные внедрения", "Инфраструктура АС/ФП", @@ -192,6 +195,56 @@ tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs( ] ) +with tab_chat: + st.subheader("Чат с ИИ") + st.caption( + f"Запросы идут на backend `{backend_base_url}/chat`, а оттуда — в Ollama (`/api/chat`). " + f"Модель: **{model}**." + ) + head_a, head_b = st.columns([4, 1]) + with head_b: + + def _clear_chat(): + st.session_state.chat_messages = [] + + st.button("Очистить чат", on_click=_clear_chat, use_container_width=True, key="btn_clear_chat") + + with st.container(border=True): + for m in st.session_state.chat_messages: + with st.chat_message(m["role"]): + st.markdown(m["content"].replace("$", r"\$")) + + user_text = st.chat_input("Напишите сообщение…", key="chat_input_portal") + if user_text: + st.session_state.chat_messages.append({"role": "user", "content": user_text}) + with st.chat_message("user"): + st.markdown(user_text.replace("$", r"\$")) + + reply = "" + with st.chat_message("assistant"): + with st.spinner("Ollama отвечает…"): + try: + r = requests.post( + f"{backend_base_url}/chat", + json={ + "model": model, + "messages": st.session_state.chat_messages, + }, + headers=_auth_headers(), + timeout=120, + ) + if r.status_code >= 400: + reply = f"**Ошибка:** {_detail_or_text(r)}" + st.error(_detail_or_text(r)) + else: + data = r.json() + reply = data.get("content") or "" + st.markdown(reply.replace("$", r"\$")) + except Exception as e: + reply = f"**Ошибка сети:** {e}" + st.error(str(e)) + st.session_state.chat_messages.append({"role": "assistant", "content": reply}) + with tab_info: st.subheader("Сведения об АС/ФП") st.caption("Черновик формы. Дальше можно привязать к Postgres.")