update
This commit is contained in:
@@ -16,7 +16,7 @@ def build_database_url() -> str:
|
|||||||
port = _env("POSTGRES_PORT", "5432")
|
port = _env("POSTGRES_PORT", "5432")
|
||||||
db = _env("POSTGRES_DB", "devops_portal")
|
db = _env("POSTGRES_DB", "devops_portal")
|
||||||
user = _env("POSTGRES_USER", "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}"
|
return f"postgresql+psycopg://{user}:{password}@{host}:{port}/{db}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import requests
|
|||||||
import yaml
|
import yaml
|
||||||
from fastapi import Depends, FastAPI, HTTPException
|
from fastapi import Depends, FastAPI, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import delete, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ from backend.auth import ( # noqa: E402
|
|||||||
verify_password,
|
verify_password,
|
||||||
)
|
)
|
||||||
from backend.db import engine, get_db # noqa: E402
|
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):
|
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}
|
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")
|
@app.post("/chat")
|
||||||
def chat_with_ollama(
|
def chat_with_ollama(
|
||||||
req: ChatRequest,
|
req: ChatRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
|
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
|
||||||
):
|
):
|
||||||
if not req.messages:
|
if not req.messages:
|
||||||
@@ -193,6 +230,29 @@ def chat_with_ollama(
|
|||||||
content = msg.get("content", "")
|
content = msg.get("content", "")
|
||||||
if not content and isinstance(data.get("response"), str):
|
if not content and isinstance(data.get("response"), str):
|
||||||
content = data["response"]
|
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}
|
return {"role": "assistant", "content": content}
|
||||||
except requests.HTTPError as e:
|
except requests.HTTPError as e:
|
||||||
detail = e.response.text if e.response is not None else str(e)
|
detail = e.response.text if e.response is not None else str(e)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
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.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
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)
|
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now, 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():
|
def _logout():
|
||||||
st.session_state.token = None
|
st.session_state.token = None
|
||||||
|
st.session_state.chat_messages = []
|
||||||
|
st.session_state.chat_hydrated = False
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
st.button(
|
st.button(
|
||||||
@@ -155,6 +157,8 @@ def _show_account():
|
|||||||
st.error(f"Ошибка входа: {_detail_or_text(r)}")
|
st.error(f"Ошибка входа: {_detail_or_text(r)}")
|
||||||
else:
|
else:
|
||||||
st.session_state.token = r.json().get("access_token")
|
st.session_state.token = r.json().get("access_token")
|
||||||
|
st.session_state.chat_messages = []
|
||||||
|
st.session_state.chat_hydrated = False
|
||||||
st.success("Успешный вход.")
|
st.success("Успешный вход.")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -180,13 +184,28 @@ if not st.session_state.token:
|
|||||||
_show_account()
|
_show_account()
|
||||||
st.stop()
|
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)
|
_title_bar(show_logout=True)
|
||||||
st.caption("Портал с данными и инструментами для АС/ФП.")
|
st.caption("Портал с данными и инструментами для АС/ФП.")
|
||||||
|
|
||||||
tab_chat, 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(
|
||||||
[
|
[
|
||||||
"Чат с ИИ",
|
"AI",
|
||||||
"Сведения об АС/ФП",
|
"Сведения об АС/ФП",
|
||||||
"Автономные внедрения",
|
"Автономные внедрения",
|
||||||
"Инфраструктура АС/ФП",
|
"Инфраструктура АС/ФП",
|
||||||
@@ -196,54 +215,57 @@ tab_chat, tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with tab_chat:
|
with tab_chat:
|
||||||
st.subheader("Чат с ИИ")
|
st.subheader("AI")
|
||||||
st.caption(
|
st.caption(
|
||||||
f"Запросы идут на backend `{backend_base_url}/chat`, а оттуда — в Ollama (`/api/chat`). "
|
f"Диалог через `{backend_base_url}/chat` → Ollama. Модель: **{model}**. "
|
||||||
f"Модель: **{model}**."
|
"Переписка сохраняется в базе для вашего пользователя."
|
||||||
)
|
)
|
||||||
head_a, head_b = st.columns([4, 1])
|
_, head_b = st.columns([4, 1])
|
||||||
with head_b:
|
with head_b:
|
||||||
|
|
||||||
def _clear_chat():
|
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.session_state.chat_messages = []
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
st.button("Очистить чат", on_click=_clear_chat, use_container_width=True, key="btn_clear_chat")
|
st.button("Очистить чат", on_click=_clear_chat, use_container_width=True, key="btn_clear_chat")
|
||||||
|
|
||||||
|
# Только лента сообщений внутри рамки; st.chat_input — снаружи, чтобы строка была внизу экрана
|
||||||
with st.container(border=True):
|
with st.container(border=True):
|
||||||
for m in st.session_state.chat_messages:
|
for m in st.session_state.chat_messages:
|
||||||
with st.chat_message(m["role"]):
|
with st.chat_message(m["role"]):
|
||||||
st.markdown(m["content"].replace("$", r"\$"))
|
st.markdown(m["content"].replace("$", r"\$"))
|
||||||
|
|
||||||
user_text = st.chat_input("Напишите сообщение…", key="chat_input_portal")
|
user_text = st.chat_input("Напишите сообщение…", key="chat_input_portal")
|
||||||
if user_text:
|
if user_text:
|
||||||
st.session_state.chat_messages.append({"role": "user", "content": user_text})
|
st.session_state.chat_messages.append({"role": "user", "content": user_text})
|
||||||
with st.chat_message("user"):
|
reply = ""
|
||||||
st.markdown(user_text.replace("$", r"\$"))
|
with st.spinner("Ollama отвечает…"):
|
||||||
|
try:
|
||||||
reply = ""
|
r = requests.post(
|
||||||
with st.chat_message("assistant"):
|
f"{backend_base_url}/chat",
|
||||||
with st.spinner("Ollama отвечает…"):
|
json={
|
||||||
try:
|
"model": model,
|
||||||
r = requests.post(
|
"messages": st.session_state.chat_messages,
|
||||||
f"{backend_base_url}/chat",
|
},
|
||||||
json={
|
headers=_auth_headers(),
|
||||||
"model": model,
|
timeout=120,
|
||||||
"messages": st.session_state.chat_messages,
|
)
|
||||||
},
|
if r.status_code >= 400:
|
||||||
headers=_auth_headers(),
|
reply = f"**Ошибка:** {_detail_or_text(r)}"
|
||||||
timeout=120,
|
else:
|
||||||
)
|
reply = r.json().get("content") or ""
|
||||||
if r.status_code >= 400:
|
except Exception as e:
|
||||||
reply = f"**Ошибка:** {_detail_or_text(r)}"
|
reply = f"**Ошибка сети:** {e}"
|
||||||
st.error(_detail_or_text(r))
|
st.session_state.chat_messages.append({"role": "assistant", "content": reply})
|
||||||
else:
|
st.rerun()
|
||||||
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:
|
with tab_info:
|
||||||
st.subheader("Сведения об АС/ФП")
|
st.subheader("Сведения об АС/ФП")
|
||||||
|
|||||||
Reference in New Issue
Block a user