#!/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)