update
This commit is contained in:
@@ -18,8 +18,16 @@ def _env(name: str, default: str) -> str:
|
|||||||
return value if value else default
|
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")
|
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")
|
REQUIRE_AUTH_FOR_GENERATE = _env("REQUIRE_AUTH_FOR_GENERATE", "true").lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
from backend.auth import ( # noqa: E402
|
from backend.auth import ( # noqa: E402
|
||||||
@@ -40,6 +48,16 @@ class GenerationRequest(BaseModel):
|
|||||||
model: str = "codellama"
|
model: str = "codellama"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessageIn(BaseModel):
|
||||||
|
role: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class ChatRequest(BaseModel):
|
||||||
|
model: str
|
||||||
|
messages: list[ChatMessageIn]
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config_text: str):
|
def validate_config(config_text: str):
|
||||||
try:
|
try:
|
||||||
yaml.safe_load(config_text)
|
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}
|
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")
|
@app.post("/generate")
|
||||||
async def generate_pipeline(
|
async def generate_pipeline(
|
||||||
req: GenerationRequest,
|
req: GenerationRequest,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ with st.sidebar:
|
|||||||
)
|
)
|
||||||
platform = st.radio("Платформа", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"])
|
platform = st.radio("Платформа", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"])
|
||||||
st.divider()
|
st.divider()
|
||||||
st.caption("Портал АС/ФП · конфигурации в разделе «Инструменты»")
|
st.caption("Портал АС/ФП · чат и генератор используют выбранную LLM модель.")
|
||||||
|
|
||||||
if "history" not in st.session_state:
|
if "history" not in st.session_state:
|
||||||
st.session_state.history = []
|
st.session_state.history = []
|
||||||
@@ -77,6 +77,8 @@ if "asfp_info" not in st.session_state:
|
|||||||
"contacts": "",
|
"contacts": "",
|
||||||
"description": "",
|
"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):
|
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)
|
_title_bar(show_logout=True)
|
||||||
st.caption("Портал с данными и инструментами для АС/ФП.")
|
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:
|
with tab_info:
|
||||||
st.subheader("Сведения об АС/ФП")
|
st.subheader("Сведения об АС/ФП")
|
||||||
st.caption("Черновик формы. Дальше можно привязать к Postgres.")
|
st.caption("Черновик формы. Дальше можно привязать к Postgres.")
|
||||||
|
|||||||
Reference in New Issue
Block a user