#!/usr/bin/env python3
"""
Веб-интерфейс для системы аудита отелей
- Дашборд со статистикой
- Управление критериями
- Чат-бот с GPT-4o-mini
- Интеграция с Graphiti и PostgreSQL
"""
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from typing import List, Dict, Optional
import psycopg2
from psycopg2.extras import RealDictCursor
from urllib.parse import unquote
import requests
import os
from datetime import datetime
# Импортируем LLM клиент
from llm_client import llm
from llm_config import get_model_info, get_available_models, ACTIVE_PROVIDER, OPENAI_CONFIG, OPENROUTER_CONFIG, OLLAMA_CONFIG
from model_providers import get_all_models
from memory_agent import memory_agent
from user_settings_manager import user_settings_manager
import json
app = FastAPI(
title="Hotel Audit Dashboard",
description="Система аудита отелей - Общественный контроль",
version="1.0.0"
)
# Конфигурация
DB_CONFIG = {
'host': "147.45.189.234",
'port': 5432,
'database': "default_db",
'user': "gen_user",
'password': unquote("2~~9_%5EkVsU%3F2%5CS")
}
# Конфигурация теперь в llm_config.py и llm_client.py
# Конфигурация для семантического поиска
BGE_API_URL = "http://147.45.146.17:8002/embed"
BGE_API_KEY = "22564b177aa73b6ac0b8642d7773350ff4c01d4983f028beff15ea247f09fa89"
def semantic_search_hotels(query: str, region: str = None, limit: int = 5):
"""Семантический поиск по эмбеддингам отелей"""
try:
# Генерируем эмбеддинг для запроса
headers = {
"X-API-Key": BGE_API_KEY,
"Content-Type": "application/json"
}
payload = {"text": [query]}
response = requests.post(BGE_API_URL, json=payload, headers=headers, timeout=30)
if response.status_code != 200:
return []
result = response.json()
query_embedding = result.get('embeddings', [[]])[0]
if not query_embedding:
return []
embedding_str = json.dumps(query_embedding)
# Строим SQL запрос с фильтрами
conn = get_db_connection()
cur = conn.cursor()
params = [embedding_str, embedding_str]
where_clause = "embedding IS NOT NULL"
if region:
where_clause += " AND metadata->>'region_name' = %s"
params.append(region)
params.append(limit)
query_sql = f"""
SELECT
metadata->>'hotel_name' as hotel_name,
metadata->>'region_name' as region_name,
metadata->>'url' as url,
LEFT(text, 500) as text,
embedding <-> %s::vector as distance
FROM hotel_website_chunks
WHERE {where_clause}
ORDER BY embedding <-> %s::vector
LIMIT %s;
"""
cur.execute(query_sql, params)
results = cur.fetchall()
cur.close()
conn.close()
return results
except Exception as e:
print(f"Ошибка семантического поиска: {e}")
return []
# Модели данных
class ChatMessage(BaseModel):
message: str
region: Optional[str] = None
group_id: Optional[str] = None
class CriterionUpdate(BaseModel):
id: int
name: str
query: str
keywords: List[str]
def get_db_connection():
"""Получить подключение к БД"""
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
# ==================== API ENDPOINTS ====================
@app.get("/", response_class=HTMLResponse)
async def root():
"""Главная страница с дашбордом"""
return """
Аудит Отелей - Общественный Контроль
🏨 Аудит Отелей - Общественный Контроль
Система мониторинга соответствия требованиям законодательства
📊 Общая статистика
-
Всего отелей в базе
🌐 Парсинг сайтов
-
Сайтов спарсено
✅ Аудит выполнен
-
Отелей проверено
📈 Средний балл
-
Из 18 критериев
🗺 Статус по регионам
Загрузка...
🗺 Выбор региона для аудита
🏨 База отелей
Загрузка...
💬 Чат-ассистент (GPT-4o-mini)
Задавайте вопросы по данным отелей. Используется semantic search по Graphiti + PostgreSQL.
Привет! Я помогу вам с анализом данных отелей. Спрашивайте!
🤖 Управление LLM моделями
Переключение между OpenAI, OpenRouter, Ollama (как в n8n)
📊 Текущая модель
Загрузка...
🔄 Переключение провайдера
🎯 Все доступные модели (динамическая загрузка)
Нажмите "Загрузить модели" для получения списка всех доступных моделей
🧪 Тест модели
Результат теста появится здесь...
🧠 Память агента
Интеграция с MCP сервером памяти агента для сохранения контекста разговоров
📊 Статус памяти
Проверка подключения...
👤 Информация о пользователе
Определение пользователя...
🔍 Поиск в памяти
Результаты поиска появятся здесь
📚 История пользователя
История разговоров появится здесь
💾 Добавить память
📋 Управление критериями аудита
18 критериев онлайн-аудита сайтов отелей
Загрузка...
"""
@app.get("/api/stats")
async def get_stats():
"""Общая статистика"""
conn = get_db_connection()
cur = conn.cursor()
# Общие данные
cur.execute("SELECT count(*) FROM hotel_main;")
total_hotels = cur.fetchone()['count']
cur.execute("SELECT count(DISTINCT hotel_id) FROM hotel_website_raw;")
crawled_sites = cur.fetchone()['count']
cur.execute("SELECT count(*) FROM hotel_audit_results;")
audited = cur.fetchone()['count']
cur.execute("SELECT avg(total_score) FROM hotel_audit_results;")
avg_score = cur.fetchone()['avg'] or 0
# По регионам
cur.execute("""
SELECT
m.region_name,
count(DISTINCT m.id) as total_hotels,
count(DISTINCT w.hotel_id) as crawled,
count(DISTINCT a.hotel_id) as audited,
avg(a.total_score) as avg_score
FROM hotel_main m
LEFT JOIN hotel_website_raw w ON m.id = w.hotel_id
LEFT JOIN hotel_audit_results a ON m.id = a.hotel_id
WHERE m.region_name IS NOT NULL
GROUP BY m.region_name
HAVING count(DISTINCT a.hotel_id) > 0
ORDER BY avg_score DESC NULLS LAST
LIMIT 20;
""")
regions = cur.fetchall()
cur.close()
conn.close()
return {
'total_hotels': total_hotels,
'crawled_sites': crawled_sites,
'audited_hotels': audited,
'avg_score': float(avg_score),
'regions': [dict(r) for r in regions]
}
@app.get("/api/regions")
async def get_regions():
"""Список регионов"""
conn = get_db_connection()
cur = conn.cursor()
cur.execute("""
SELECT region_name, count(*) as count
FROM hotel_main
WHERE region_name IS NOT NULL
GROUP BY region_name
ORDER BY count DESC;
""")
regions = [{'name': r['region_name'], 'count': r['count']} for r in cur.fetchall()]
cur.close()
conn.close()
return regions
@app.get("/api/hotels")
async def get_hotels(search: str = '', limit: int = 100):
"""Список отелей"""
conn = get_db_connection()
cur = conn.cursor()
query = """
SELECT
m.id,
m.full_name as name,
m.region_name as region,
m.category_name as category,
r.main_data->>'websiteAddress' as website,
a.total_score as audit_score
FROM hotel_main m
LEFT JOIN hotel_raw_json r ON m.id = r.hotel_id
LEFT JOIN hotel_audit_results a ON m.id = a.hotel_id
WHERE m.full_name ILIKE %s
ORDER BY m.full_name
LIMIT %s;
"""
cur.execute(query, (f'%{search}%', limit))
hotels = [dict(r) for r in cur.fetchall()]
cur.close()
conn.close()
return hotels
@app.post("/api/chat")
async def chat(request: Request, message: ChatMessage):
"""Чат-бот с GPT-4o-mini с полным контекстом и памятью агента"""
try:
# Получаем user_id для памяти
user_id = memory_agent.get_user_id(request)
# Ищем в памяти агента релевантную информацию
memory_context = ""
try:
memory_search = mcp_memory_search_memory_facts(query=message.message, group_ids=[user_id], max_facts=3)
if memory_search:
memory_context = "\n\nРЕЛЕВАНТНАЯ ИНФОРМАЦИЯ ИЗ ПАМЯТИ:\n"
for fact in memory_search[:3]:
memory_context += f"- {fact.get('content', '')}\n"
except Exception as e:
print(f"Ошибка поиска в памяти: {e}")
# 🔍 СЕМАНТИЧЕСКИЙ ПОИСК по эмбеддингам
semantic_results = []
semantic_context = ""
try:
semantic_results = semantic_search_hotels(message.message, region=message.region, limit=5)
if semantic_results:
semantic_context = "\n\n🔍 РЕЛЕВАНТНАЯ ИНФОРМАЦИЯ ИЗ СЕМАНТИЧЕСКОГО ПОИСКА:\n"
for idx, result in enumerate(semantic_results, 1):
distance = result.get('distance', 1.0)
relevance = "🟢 Высокая" if distance < 0.9 else "🟡 Средняя" if distance < 1.0 else "🔴 Низкая"
semantic_context += f"\n{idx}. {result.get('hotel_name', 'Неизвестный отель')} ({result.get('region_name', 'Неизвестный регион')})\n"
semantic_context += f" Релевантность: {relevance} (расстояние: {distance:.3f})\n"
semantic_context += f" URL: {result.get('url', 'Нет URL')}\n"
semantic_context += f" Текст: {result.get('text', 'Нет текста')[:300]}...\n"
except Exception as e:
print(f"Ошибка семантического поиска: {e}")
conn = get_db_connection()
cur = conn.cursor()
# Ищем упоминания регионов в вопросе
cur.execute("""
SELECT DISTINCT region_name
FROM hotel_main
WHERE region_name IS NOT NULL
ORDER BY region_name;
""")
all_regions = [r['region_name'] for r in cur.fetchall()]
# Находим упомянутые регионы (улучшенный поиск)
mentioned_regions = []
message_lower = message.message.lower()
for region in all_regions:
region_lower = region.lower()
# Ищем частичные совпадения
if any(word in region_lower for word in message_lower.split() if len(word) > 3):
mentioned_regions.append(region)
# Специальные случаи
elif 'чукот' in message_lower and 'чукот' in region_lower:
mentioned_regions.append(region)
elif 'питер' in message_lower and ('санкт-петербург' in region_lower or 'ленинград' in region_lower):
mentioned_regions.append(region)
elif 'москв' in message_lower and 'москв' in region_lower:
mentioned_regions.append(region)
# Если регион упомянут - даём полную статистику
context_hotels = []
if mentioned_regions:
for region in mentioned_regions[:3]: # Макс 3 региона
cur.execute("""
SELECT m.full_name, m.region_name,
r.main_data->>'websiteAddress' as website,
a.total_score, a.has_website
FROM hotel_main m
LEFT JOIN hotel_raw_json r ON m.id = r.hotel_id
LEFT JOIN hotel_audit_results a ON m.id = a.hotel_id
WHERE m.region_name = %s
ORDER BY a.total_score DESC NULLS LAST
LIMIT 50;
""", (region,))
context_hotels.extend(cur.fetchall())
else:
# Общий поиск
cur.execute("""
SELECT m.full_name, m.region_name,
r.main_data->>'websiteAddress' as website,
a.total_score, a.has_website
FROM hotel_main m
LEFT JOIN hotel_raw_json r ON m.id = r.hotel_id
LEFT JOIN hotel_audit_results a ON m.id = a.hotel_id
WHERE m.full_name ILIKE %s
LIMIT 10;
""", (f'%{message.message}%',))
context_hotels = cur.fetchall()
# Статистика
cur.execute("SELECT count(*) as total FROM hotel_main;")
total = cur.fetchone()['total']
# Получаем статистику по регионам для контекста
cur.execute("""
SELECT m.region_name, count(*) as count,
count(r.hotel_id) as processed,
count(a.hotel_id) as audited
FROM hotel_main m
LEFT JOIN hotel_raw_json r ON m.id = r.hotel_id
LEFT JOIN hotel_audit_results a ON m.id = a.hotel_id
WHERE m.region_name ILIKE %s
GROUP BY m.region_name
LIMIT 5;
""", (f'%{message.message}%',))
region_stats = cur.fetchall()
cur.close()
conn.close()
# Формируем детальный контекст
context = f"""Ты - ассистент для анализа данных отелей России.
БАЗА ДАННЫХ:
- Всего отелей в РФ: {total}
- Обработано детально: ~31,000
- Регионов: 85
{memory_context}
{semantic_context}"""
# Детальная статистика по упомянутым регионам
if mentioned_regions:
context += "\nСТАТИСТИКА ПО УПОМЯНУТЫМ РЕГИОНАМ:\n"
for region in mentioned_regions:
with_sites = sum(1 for h in context_hotels if h['region_name'] == region and h['has_website'])
without_sites = sum(1 for h in context_hotels if h['region_name'] == region and not h['has_website'])
total_reg = with_sites + without_sites
if total_reg > 0:
context += f"\n{region}:\n"
context += f"- Всего отелей: {total_reg}\n"
context += f"- С сайтами: {with_sites} ({with_sites/total_reg*100:.1f}%)\n"
context += f"- БЕЗ сайтов: {without_sites} ({without_sites/total_reg*100:.1f}%)\n"
if context_hotels:
context += "\nОТЕЛИ (детально):\n"
for i, h in enumerate(context_hotels[:30]): # Ограничиваем для промпта
context += f"\n{i+1}. {h['full_name']}\n"
context += f" Регион: {h['region_name']}\n"
if h['has_website']:
context += f" Сайт: {h['website'] or 'указан'}\n"
if h['total_score'] is not None:
context += f" Балл аудита: {h['total_score']}/18\n"
else:
context += f" Сайт: НЕТ (балл аудита: 0/18 автоматически)\n"
context += """
ВАЖНО:
- Если у отеля НЕТ сайта - автоматически 0/18 баллов по всем критериям
- Отвечай точно на основе предоставленных данных
- Если спрашивают про конкретный регион - используй статистику выше
"""
# Запрос к LLM через универсальный клиент
messages = [
{"role": "system", "content": context},
{"role": "user", "content": message.message}
]
result = llm.chat_completion(messages)
response_text = result['text']
# Сохраняем диалог в память агента
try:
conversation_content = f"Пользователь: {message.message}\nАссистент: {response_text}"
mcp_memory_add_memory(
name=f"Chat with {user_id}",
episode_body=conversation_content,
group_id=user_id,
source="chat",
source_description=f"Chat conversation with user {user_id}"
)
except Exception as e:
print(f"Ошибка сохранения в память: {e}")
return {"response": response_text}
except Exception as e:
return {"response": f"Ошибка: {str(e)}"}
@app.get("/api/criteria")
async def get_criteria():
"""Список критериев аудита"""
from audit_system import AUDIT_CRITERIA
return AUDIT_CRITERIA
@app.post("/api/audit/run")
async def run_audit(request: dict):
"""Запустить аудит региона в фоне"""
region = request.get('region')
force_recrawl = request.get('force_recrawl', False)
force_audit = request.get('force_audit', False)
# Проверяем есть ли уже спарсенные данные
try:
cur = get_db_connection().cursor()
# Считаем отели с сайтами и обработанные
cur.execute('''
SELECT
COUNT(CASE WHEN h.website_address IS NOT NULL AND h.website_address != '' AND h.website_address != '-' THEN 1 END) as with_websites,
COUNT(DISTINCT w.hotel_id) as crawled_count
FROM hotel_main h
LEFT JOIN hotel_website_raw w ON h.id = w.hotel_id
WHERE h.region_name ILIKE %s
''', (f'%{region}%',))
result = cur.fetchone()
with_websites = result['with_websites'] if result else 0
crawled_count = result['crawled_count'] if result else 0
cur.close()
# Определяем что делать
if crawled_count == 0 and with_websites > 0:
# Нет данных - запускаем краулер
import subprocess
subprocess.Popen([
'python', 'universal_crawler.py', region
], cwd='/root/engine/public_oversight/hotels',
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return {
"status": "crawler_started",
"region": region,
"hotels_to_crawl": with_websites,
"message": f"🚀 Запущен краулинг {with_websites} отелей. Примерное время: {with_websites * 7 / 60:.1f} минут. После завершения запустите аудит повторно."
}
elif crawled_count > 0 and crawled_count < with_websites and not force_recrawl and not force_audit:
# Частично обработано - спрашиваем что делать
return {
"status": "partial_data",
"region": region,
"crawled": crawled_count,
"total": with_websites,
"message": f"⚠️ Обработано {crawled_count} из {with_websites} отелей. Добавьте 'force_audit': true для запуска аудита с частичными данными."
}
elif crawled_count >= with_websites or force_recrawl or force_audit:
# Данные есть - запускаем аудит
import subprocess
subprocess.Popen([
'python', 'audit_system.py', region, f"hotel_{region.replace(' ', '_').lower()}"
], cwd='/root/engine/public_oversight/hotels')
return {
"status": "audit_started",
"region": region,
"hotels_count": crawled_count,
"message": f"✅ Аудит запущен для {crawled_count} отелей"
}
else:
return {
"status": "no_websites",
"region": region,
"message": f"В регионе нет отелей с указанными сайтами"
}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.get("/api/audit/download/{region}")
async def download_audit(region: str):
"""Скачать Excel отчет по аудиту"""
import os
from fastapi.responses import FileResponse
# Ищем последний файл аудита для региона
region_safe = region.replace(' ', '_')
audit_dir = '/root/engine/public_oversight/hotels'
try:
files = [f for f in os.listdir(audit_dir) if f.startswith(f'audit_{region_safe}') and f.endswith('.xlsx')]
if not files:
return {"error": f"Файл аудита для региона '{region}' не найден. Сначала запустите аудит."}
# Берем последний файл (по дате в имени)
files.sort(reverse=True)
latest_file = files[0]
file_path = os.path.join(audit_dir, latest_file)
return FileResponse(
path=file_path,
filename=latest_file,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
except Exception as e:
return {"error": str(e)}
@app.post("/api/audit/criteria-stats")
async def get_criteria_stats(request: dict):
"""Статистика по критериям аудита для региона"""
conn = get_db_connection()
cur = conn.cursor()
try:
region = request.get('region', '')
if not region:
return {"status": "error", "error": "Параметр region обязателен"}
# Получаем все результаты аудита для региона
audit_query = """
SELECT criteria_results
FROM hotel_audit_results
WHERE region_name ILIKE %s
"""
cur.execute(audit_query, (f'%{region}%',))
audit_results = cur.fetchall()
if not audit_results:
return {"status": "error", "error": "Нет данных аудита для региона"}
# Собираем статистику по критериям
criteria_stats = {}
for row in audit_results:
criteria_results = row['criteria_results']
if criteria_results:
for criterion_key, criterion_data in criteria_results.items():
# Получаем название критерия и результат
criterion_name = criterion_data.get('name', criterion_key)
verdict = criterion_data.get('verdict', 'НЕТ')
if criterion_name not in criteria_stats:
criteria_stats[criterion_name] = {
'criterion_name': criterion_name,
'total_count': 0,
'yes_count': 0,
'partial_count': 0,
'no_count': 0
}
criteria_stats[criterion_name]['total_count'] += 1
if verdict == 'ДА':
criteria_stats[criterion_name]['yes_count'] += 1
elif verdict == 'ЧАСТИЧНО':
criteria_stats[criterion_name]['partial_count'] += 1
else: # НЕТ
criteria_stats[criterion_name]['no_count'] += 1
# Конвертируем в список и добавляем проценты
criteria_list = []
for criterion_data in criteria_stats.values():
if criterion_data['total_count'] > 0:
yes_percentage = (criterion_data['yes_count'] / criterion_data['total_count']) * 100
partial_percentage = (criterion_data['partial_count'] / criterion_data['total_count']) * 100
no_percentage = (criterion_data['no_count'] / criterion_data['total_count']) * 100
criteria_list.append({
'criterion_name': criterion_data['criterion_name'],
'total_count': criterion_data['total_count'],
'yes_count': criterion_data['yes_count'],
'partial_count': criterion_data['partial_count'],
'no_count': criterion_data['no_count'],
'yes_percentage': round(yes_percentage, 1),
'partial_percentage': round(partial_percentage, 1),
'no_percentage': round(no_percentage, 1)
})
# Сортируем по проценту "ДА"
criteria_list.sort(key=lambda x: x['yes_percentage'], reverse=True)
cur.close()
conn.close()
return {
"status": "success",
"region": region,
"criteria_stats": criteria_list
}
except Exception as e:
cur.close()
conn.close()
return {"status": "error", "error": str(e)}
@app.get("/api/llm/info")
async def get_llm_info():
"""Информация о текущей LLM модели"""
return get_model_info()
@app.get("/api/llm/models")
async def get_llm_models():
"""Список доступных моделей"""
return {
"current_provider": llm.provider,
"models": get_available_models(),
"current_model": llm.model
}
@app.post("/api/llm/switch")
async def switch_llm_model(request: dict):
"""Сменить модель на лету"""
model = request.get('model')
if not model:
raise HTTPException(400, "model parameter required")
# Сохраняем модель в переменной окружения
os.environ['LLM_MODEL'] = model
# Меняем модель
llm.model = model
return {
"status": "switched",
"new_model": model,
"provider": llm.provider,
"provider_config": llm.provider_config
}
@app.post("/api/llm/switch-provider")
async def switch_provider(request: dict):
"""Переключить провайдера (OpenAI/OpenRouter/Ollama)"""
provider = request.get('provider')
if provider not in ['openai', 'openrouter', 'ollama']:
raise HTTPException(400, "Invalid provider")
# Сохраняем в переменную окружения
os.environ['ACTIVE_PROVIDER'] = provider
# Обновляем глобальную переменную
import llm_config
llm_config.ACTIVE_PROVIDER = provider
# Перезагружаем клиент
from llm_client import LLMClient
global llm
llm = LLMClient()
return {
"status": "provider_switched",
"new_provider": provider,
"current_model": llm.model,
"provider_config": llm.provider_config
}
# ===== ЭНДПОИНТЫ ДЛЯ ПАМЯТИ АГЕНТА =====
@app.get("/api/memory/status")
async def memory_status():
"""Статус MCP сервера памяти"""
try:
# Тестируем через MCP инструменты
return {"status": "success", "message": "MCP сервер памяти доступен"}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.get("/api/memory/history/{user_id}")
async def get_memory_history(user_id: str, last_n: int = 10):
"""Получить историю пользователя из памяти"""
try:
# Используем MCP инструменты напрямую
result = mcp_memory_get_episodes(group_id=user_id, last_n=last_n)
return {"status": "success", "data": result}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.post("/api/memory/search")
async def search_memory(data: dict):
"""Поиск в памяти агента"""
try:
user_id = data.get("user_id")
query = data.get("query", "")
max_results = data.get("max_results", 10)
if not user_id:
return {"status": "error", "message": "user_id обязателен"}
# Используем MCP инструменты напрямую
result = mcp_memory_search_memory_facts(query=query, group_ids=[user_id], max_facts=max_results)
return {"status": "success", "data": result}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.post("/api/memory/add")
async def add_memory(request: Request, data: dict):
"""Добавить память в агента"""
try:
user_id = memory_agent.get_user_id(request)
content = data.get("content", "")
source = data.get("source", "chat")
metadata = data.get("metadata", {})
if not content:
return {"status": "error", "message": "content обязателен"}
# Используем MCP инструменты напрямую
result = mcp_memory_add_memory(
name=f"Chat with {user_id}",
episode_body=content,
group_id=user_id,
source=source,
source_description=f"Chat conversation with user {user_id}"
)
return {"status": "success", "data": result}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.get("/api/llm/providers")
async def get_providers():
"""Получить список провайдеров из БД"""
try:
providers = user_settings_manager.get_providers()
# Добавляем информацию о ключах API
provider_info = {
'openai': {'has_key': bool(OPENAI_CONFIG.get('api_key')), 'api_base': OPENAI_CONFIG['api_base']},
'openrouter': {'has_key': bool(OPENROUTER_CONFIG.get('api_key')), 'api_base': OPENROUTER_CONFIG['api_base']},
'ollama': {'has_key': False, 'api_base': OLLAMA_CONFIG['api_base']}
}
available = []
for provider in providers:
info = provider_info.get(provider['provider'], {'has_key': False, 'api_base': ''})
available.append({
"id": provider['provider'],
"name": provider['provider'].upper(),
"description": f"{provider['model_count']} моделей",
"api_base": info['api_base'],
"has_key": info['has_key'],
"model_count": provider['model_count']
})
return {
"current": ACTIVE_PROVIDER,
"available": available
}
except Exception as e:
return {"error": str(e)}
@app.get("/api/llm/models-dynamic")
async def get_dynamic_models():
"""Получить все модели от всех провайдеров из БД"""
try:
all_models = user_settings_manager.get_available_models()
# Группируем по провайдерам
providers = {}
for model in all_models:
provider = model['provider']
if provider not in providers:
providers[provider] = []
providers[provider].append(model)
return {
"status": "success",
"providers": providers,
"total_models": len(all_models)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"providers": {},
"total_models": 0
}
@app.post("/api/llm/save-user-settings")
async def save_user_settings(request: Request, data: dict):
"""Сохранить настройки пользователя в БД"""
try:
user_id = memory_agent.get_user_id(request)
provider = data.get('provider')
model = data.get('model')
if not provider or not model:
return {"status": "error", "message": "provider и model обязательны"}
# Сохраняем в БД
success = user_settings_manager.set_user_llm_settings(user_id, provider, model)
if success:
# Обновляем глобальные настройки
os.environ['ACTIVE_PROVIDER'] = provider
os.environ['LLM_MODEL'] = model
# Перезагружаем клиент
from llm_client import LLMClient
global llm
llm = LLMClient()
return {
"status": "success",
"message": "Настройки сохранены в БД",
"user_id": user_id,
"provider": provider,
"model": model
}
else:
return {"status": "error", "message": "Ошибка сохранения в БД"}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.get("/api/llm/user-settings")
async def get_user_settings(request: Request):
"""Получить настройки пользователя из БД"""
try:
user_id = memory_agent.get_user_id(request)
settings = user_settings_manager.get_user_llm_settings(user_id)
return {
"status": "success",
"user_id": user_id,
"settings": settings
}
except Exception as e:
return {"status": "error", "error": str(e)}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8888)