update
This commit is contained in:
@@ -16,7 +16,7 @@ def build_database_url() -> str:
|
||||
port = _env("POSTGRES_PORT", "5432")
|
||||
db = _env("POSTGRES_DB", "devops_portal")
|
||||
user = _env("POSTGRES_USER", "devops_portal")
|
||||
password = _env("POSTGRES_PASSWORD", "change-me")
|
||||
password = _env("POSTGRES_PASSWORD", "ADzXo0Gm!")
|
||||
return f"postgresql+psycopg://{user}:{password}@{host}:{port}/{db}"
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import requests
|
||||
import yaml
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
@@ -39,7 +39,7 @@ from backend.auth import ( # noqa: E402
|
||||
verify_password,
|
||||
)
|
||||
from backend.db import engine, get_db # noqa: E402
|
||||
from backend.models import Base, User # noqa: E402
|
||||
from backend.models import Base, User, UserChatMessage # noqa: E402
|
||||
|
||||
|
||||
class GenerationRequest(BaseModel):
|
||||
@@ -169,9 +169,46 @@ 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.get("/chat/history")
|
||||
def chat_history(
|
||||
limit: int = 500,
|
||||
db: Session = Depends(get_db),
|
||||
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
|
||||
):
|
||||
if user is None:
|
||||
return {"messages": []}
|
||||
lim = max(1, min(limit, 2000))
|
||||
rows = (
|
||||
db.execute(
|
||||
select(UserChatMessage)
|
||||
.where(UserChatMessage.user_id == user.id)
|
||||
.order_by(UserChatMessage.created_at.asc())
|
||||
.limit(lim)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return {
|
||||
"messages": [{"role": r.role, "content": r.content} for r in rows],
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/chat/history")
|
||||
def chat_history_clear(
|
||||
db: Session = Depends(get_db),
|
||||
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
|
||||
):
|
||||
if user is None:
|
||||
return {"ok": True}
|
||||
db.execute(delete(UserChatMessage).where(UserChatMessage.user_id == user.id))
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/chat")
|
||||
def chat_with_ollama(
|
||||
req: ChatRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
|
||||
):
|
||||
if not req.messages:
|
||||
@@ -193,6 +230,29 @@ def chat_with_ollama(
|
||||
content = msg.get("content", "")
|
||||
if not content and isinstance(data.get("response"), str):
|
||||
content = data["response"]
|
||||
|
||||
if user is not None:
|
||||
last = req.messages[-1]
|
||||
if last.role == "user":
|
||||
try:
|
||||
db.add(
|
||||
UserChatMessage(
|
||||
user_id=user.id,
|
||||
role="user",
|
||||
content=last.content,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
UserChatMessage(
|
||||
user_id=user.id,
|
||||
role="assistant",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
|
||||
return {"role": "assistant", "content": content}
|
||||
except requests.HTTPError as e:
|
||||
detail = e.response.text if e.response is not None else str(e)
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, String
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
@@ -27,3 +27,19 @@ class User(Base):
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now, nullable=False)
|
||||
|
||||
|
||||
class UserChatMessage(Base):
|
||||
"""История чата с ИИ (Ollama) по пользователю."""
|
||||
|
||||
__tablename__ = "user_chat_messages"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
role: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now, nullable=False)
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ def _title_bar(*, show_logout: bool):
|
||||
|
||||
def _logout():
|
||||
st.session_state.token = None
|
||||
st.session_state.chat_messages = []
|
||||
st.session_state.chat_hydrated = False
|
||||
st.rerun()
|
||||
|
||||
st.button(
|
||||
@@ -155,6 +157,8 @@ def _show_account():
|
||||
st.error(f"Ошибка входа: {_detail_or_text(r)}")
|
||||
else:
|
||||
st.session_state.token = r.json().get("access_token")
|
||||
st.session_state.chat_messages = []
|
||||
st.session_state.chat_hydrated = False
|
||||
st.success("Успешный вход.")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
@@ -180,13 +184,28 @@ if not st.session_state.token:
|
||||
_show_account()
|
||||
st.stop()
|
||||
|
||||
# Подтянуть историю чата из Postgres (один раз после входа / смены пользователя)
|
||||
if not st.session_state.get("chat_hydrated"):
|
||||
try:
|
||||
hr = requests.get(
|
||||
f"{backend_base_url}/chat/history",
|
||||
headers=_auth_headers(),
|
||||
params={"limit": 500},
|
||||
timeout=30,
|
||||
)
|
||||
if hr.ok:
|
||||
st.session_state.chat_messages = hr.json().get("messages", [])
|
||||
except Exception:
|
||||
pass
|
||||
st.session_state.chat_hydrated = True
|
||||
|
||||
# После входа — тот же заголовок + вкладки
|
||||
_title_bar(show_logout=True)
|
||||
st.caption("Портал с данными и инструментами для АС/ФП.")
|
||||
|
||||
tab_chat, tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
|
||||
[
|
||||
"Чат с ИИ",
|
||||
"AI",
|
||||
"Сведения об АС/ФП",
|
||||
"Автономные внедрения",
|
||||
"Инфраструктура АС/ФП",
|
||||
@@ -196,19 +215,29 @@ tab_chat, tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
|
||||
)
|
||||
|
||||
with tab_chat:
|
||||
st.subheader("Чат с ИИ")
|
||||
st.subheader("AI")
|
||||
st.caption(
|
||||
f"Запросы идут на backend `{backend_base_url}/chat`, а оттуда — в Ollama (`/api/chat`). "
|
||||
f"Модель: **{model}**."
|
||||
f"Диалог через `{backend_base_url}/chat` → Ollama. Модель: **{model}**. "
|
||||
"Переписка сохраняется в базе для вашего пользователя."
|
||||
)
|
||||
head_a, head_b = st.columns([4, 1])
|
||||
_, head_b = st.columns([4, 1])
|
||||
with head_b:
|
||||
|
||||
def _clear_chat():
|
||||
try:
|
||||
requests.delete(
|
||||
f"{backend_base_url}/chat/history",
|
||||
headers=_auth_headers(),
|
||||
timeout=30,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
st.session_state.chat_messages = []
|
||||
st.rerun()
|
||||
|
||||
st.button("Очистить чат", on_click=_clear_chat, use_container_width=True, key="btn_clear_chat")
|
||||
|
||||
# Только лента сообщений внутри рамки; st.chat_input — снаружи, чтобы строка была внизу экрана
|
||||
with st.container(border=True):
|
||||
for m in st.session_state.chat_messages:
|
||||
with st.chat_message(m["role"]):
|
||||
@@ -217,11 +246,7 @@ with tab_chat:
|
||||
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(
|
||||
@@ -235,15 +260,12 @@ with tab_chat:
|
||||
)
|
||||
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"\$"))
|
||||
reply = r.json().get("content") or ""
|
||||
except Exception as e:
|
||||
reply = f"**Ошибка сети:** {e}"
|
||||
st.error(str(e))
|
||||
st.session_state.chat_messages.append({"role": "assistant", "content": reply})
|
||||
st.rerun()
|
||||
|
||||
with tab_info:
|
||||
st.subheader("Сведения об АС/ФП")
|
||||
|
||||
Reference in New Issue
Block a user