Added code samples for AI-Agents
This commit is contained in:
13
services/__init__.py
Normal file
13
services/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Сервисы для AI-агентов."""
|
||||
from services.cache_service import CacheService
|
||||
from services.data_analyzer import DataAnalyzer
|
||||
from services.image_processor import ImageProcessor
|
||||
from services.token_manager import TokenManager
|
||||
|
||||
__all__ = [
|
||||
"TokenManager",
|
||||
"CacheService",
|
||||
"ImageProcessor",
|
||||
"DataAnalyzer",
|
||||
]
|
||||
|
||||
100
services/cache_service.py
Normal file
100
services/cache_service.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Сервис кэширования для Redis."""
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import redis.asyncio as redis
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""Сервис для работы с Redis кэшем."""
|
||||
|
||||
def __init__(self, redis_url: Optional[str] = None):
|
||||
self.redis_url = redis_url or "redis://localhost:6379/0"
|
||||
self._client: Optional[redis.Redis] = None
|
||||
|
||||
async def _get_client(self) -> redis.Redis:
|
||||
"""Получить клиент Redis (lazy initialization)."""
|
||||
if self._client is None:
|
||||
self._client = await redis.from_url(self.redis_url, decode_responses=True)
|
||||
return self._client
|
||||
|
||||
async def get_context(self, conversation_id: str, max_messages: int = 50) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Получить контекст разговора из кэша.
|
||||
|
||||
Args:
|
||||
conversation_id: ID разговора
|
||||
max_messages: Максимальное количество сообщений
|
||||
|
||||
Returns:
|
||||
Список сообщений в формате [{"role": "...", "content": "..."}]
|
||||
"""
|
||||
client = await self._get_client()
|
||||
key = f"conversation:{conversation_id}"
|
||||
|
||||
data = await client.get(key)
|
||||
if not data:
|
||||
return []
|
||||
|
||||
messages = json.loads(data)
|
||||
# Возвращаем последние N сообщений
|
||||
return messages[-max_messages:] if len(messages) > max_messages else messages
|
||||
|
||||
async def save_context(self, conversation_id: str, messages: List[Dict[str, str]], ttl: int = 86400):
|
||||
"""
|
||||
Сохранить контекст разговора в кэш.
|
||||
|
||||
Args:
|
||||
conversation_id: ID разговора
|
||||
messages: Список сообщений
|
||||
ttl: Время жизни в секундах (по умолчанию 24 часа)
|
||||
"""
|
||||
client = await self._get_client()
|
||||
key = f"conversation:{conversation_id}"
|
||||
|
||||
# Ограничиваем количество сообщений для экономии памяти
|
||||
max_messages = 100
|
||||
if len(messages) > max_messages:
|
||||
messages = messages[-max_messages:]
|
||||
|
||||
await client.setex(key, ttl, json.dumps(messages, ensure_ascii=False))
|
||||
|
||||
async def add_message(self, conversation_id: str, role: str, content: str):
|
||||
"""
|
||||
Добавить сообщение в контекст разговора.
|
||||
|
||||
Args:
|
||||
conversation_id: ID разговора
|
||||
role: Роль (user, assistant, system)
|
||||
content: Содержимое сообщения
|
||||
"""
|
||||
messages = await self.get_context(conversation_id, max_messages=1000)
|
||||
messages.append({"role": role, "content": content})
|
||||
await self.save_context(conversation_id, messages)
|
||||
|
||||
async def clear_context(self, conversation_id: str):
|
||||
"""Очистить контекст разговора."""
|
||||
client = await self._get_client()
|
||||
key = f"conversation:{conversation_id}"
|
||||
await client.delete(key)
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
"""Получить значение по ключу."""
|
||||
client = await self._get_client()
|
||||
data = await client.get(key)
|
||||
return json.loads(data) if data else None
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: int = 3600):
|
||||
"""Установить значение с TTL."""
|
||||
client = await self._get_client()
|
||||
await client.setex(key, ttl, json.dumps(value, ensure_ascii=False))
|
||||
|
||||
async def close(self):
|
||||
"""Закрыть соединение с Redis."""
|
||||
if self._client:
|
||||
await self._client.close()
|
||||
self._client = None
|
||||
|
||||
156
services/data_analyzer.py
Normal file
156
services/data_analyzer.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Сервис анализа данных детей."""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class DataAnalyzer:
|
||||
"""Сервис для анализа прогресса детей."""
|
||||
|
||||
@staticmethod
|
||||
def calculate_completion_rate(tasks: List[Dict]) -> float:
|
||||
"""
|
||||
Рассчитать процент выполнения заданий.
|
||||
|
||||
Args:
|
||||
tasks: Список заданий с полем 'completed'
|
||||
|
||||
Returns:
|
||||
Процент выполнения (0.0 - 1.0)
|
||||
"""
|
||||
if not tasks:
|
||||
return 0.0
|
||||
|
||||
completed = sum(1 for task in tasks if task.get("completed", False))
|
||||
return completed / len(tasks)
|
||||
|
||||
@staticmethod
|
||||
def analyze_daily_progress(schedules: List[Dict]) -> Dict:
|
||||
"""
|
||||
Проанализировать ежедневный прогресс.
|
||||
|
||||
Args:
|
||||
schedules: Список расписаний с заданиями
|
||||
|
||||
Returns:
|
||||
Словарь с аналитикой
|
||||
"""
|
||||
if not schedules:
|
||||
return {
|
||||
"total_days": 0,
|
||||
"average_completion": 0.0,
|
||||
"total_tasks": 0,
|
||||
"completed_tasks": 0,
|
||||
}
|
||||
|
||||
total_tasks = 0
|
||||
completed_tasks = 0
|
||||
completion_rates = []
|
||||
|
||||
for schedule in schedules:
|
||||
tasks = schedule.get("tasks", [])
|
||||
total_tasks += len(tasks)
|
||||
completed_tasks += sum(1 for task in tasks if task.get("completed", False))
|
||||
rate = DataAnalyzer.calculate_completion_rate(tasks)
|
||||
completion_rates.append(rate)
|
||||
|
||||
return {
|
||||
"total_days": len(schedules),
|
||||
"average_completion": sum(completion_rates) / len(completion_rates) if completion_rates else 0.0,
|
||||
"total_tasks": total_tasks,
|
||||
"completed_tasks": completed_tasks,
|
||||
"completion_rate": completed_tasks / total_tasks if total_tasks > 0 else 0.0,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_category_statistics(schedules: List[Dict]) -> Dict[str, Dict]:
|
||||
"""
|
||||
Получить статистику по категориям заданий.
|
||||
|
||||
Args:
|
||||
schedules: Список расписаний
|
||||
|
||||
Returns:
|
||||
Словарь со статистикой по категориям
|
||||
"""
|
||||
category_stats: Dict[str, Dict] = {}
|
||||
|
||||
for schedule in schedules:
|
||||
for task in schedule.get("tasks", []):
|
||||
category = task.get("category", "unknown")
|
||||
if category not in category_stats:
|
||||
category_stats[category] = {
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
"average_duration": 0.0,
|
||||
"durations": [],
|
||||
}
|
||||
|
||||
stats = category_stats[category]
|
||||
stats["total"] += 1
|
||||
if task.get("completed", False):
|
||||
stats["completed"] += 1
|
||||
if "duration_minutes" in task:
|
||||
stats["durations"].append(task["duration_minutes"])
|
||||
|
||||
# Вычисляем среднюю длительность
|
||||
for category, stats in category_stats.items():
|
||||
if stats["durations"]:
|
||||
stats["average_duration"] = sum(stats["durations"]) / len(stats["durations"])
|
||||
del stats["durations"]
|
||||
|
||||
return category_stats
|
||||
|
||||
@staticmethod
|
||||
def get_weekly_trend(schedules: List[Dict], days: int = 7) -> List[Dict]:
|
||||
"""
|
||||
Получить тренд за последние N дней.
|
||||
|
||||
Args:
|
||||
schedules: Список расписаний
|
||||
days: Количество дней
|
||||
|
||||
Returns:
|
||||
Список словарей с данными по дням
|
||||
"""
|
||||
end_date = datetime.now().date()
|
||||
start_date = end_date - timedelta(days=days - 1)
|
||||
|
||||
# Группируем расписания по датам
|
||||
daily_data: Dict[str, List[Dict]] = {}
|
||||
for schedule in schedules:
|
||||
schedule_date = schedule.get("date")
|
||||
if isinstance(schedule_date, str):
|
||||
schedule_date = datetime.fromisoformat(schedule_date).date()
|
||||
elif isinstance(schedule_date, datetime):
|
||||
schedule_date = schedule_date.date()
|
||||
|
||||
if start_date <= schedule_date <= end_date:
|
||||
date_str = str(schedule_date)
|
||||
if date_str not in daily_data:
|
||||
daily_data[date_str] = []
|
||||
daily_data[date_str].append(schedule)
|
||||
|
||||
# Формируем тренд
|
||||
trend = []
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
date_str = str(current_date)
|
||||
day_schedules = daily_data.get(date_str, [])
|
||||
all_tasks = []
|
||||
for sched in day_schedules:
|
||||
all_tasks.extend(sched.get("tasks", []))
|
||||
|
||||
trend.append(
|
||||
{
|
||||
"date": date_str,
|
||||
"completion_rate": DataAnalyzer.calculate_completion_rate(all_tasks),
|
||||
"total_tasks": len(all_tasks),
|
||||
"completed_tasks": sum(1 for task in all_tasks if task.get("completed", False)),
|
||||
}
|
||||
)
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return trend
|
||||
|
||||
100
services/image_processor.py
Normal file
100
services/image_processor.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Сервис обработки изображений."""
|
||||
import io
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
"""Сервис для обработки изображений заданий."""
|
||||
|
||||
MAX_SIZE = (800, 800)
|
||||
SUPPORTED_FORMATS = {"JPEG", "PNG", "WEBP"}
|
||||
QUALITY = 85
|
||||
|
||||
@staticmethod
|
||||
def resize_image(
|
||||
image_data: bytes, max_size: Tuple[int, int] = MAX_SIZE, quality: int = QUALITY
|
||||
) -> bytes:
|
||||
"""
|
||||
Изменить размер изображения.
|
||||
|
||||
Args:
|
||||
image_data: Байты изображения
|
||||
max_size: Максимальный размер (width, height)
|
||||
quality: Качество JPEG (1-100)
|
||||
|
||||
Returns:
|
||||
Байты обработанного изображения
|
||||
"""
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
image_format = image.format or "JPEG"
|
||||
|
||||
# Конвертируем в RGB если нужно
|
||||
if image_format == "PNG" and image.mode in ("RGBA", "LA"):
|
||||
background = Image.new("RGB", image.size, (255, 255, 255))
|
||||
if image.mode == "RGBA":
|
||||
background.paste(image, mask=image.split()[3])
|
||||
else:
|
||||
background.paste(image)
|
||||
image = background
|
||||
elif image.mode != "RGB":
|
||||
image = image.convert("RGB")
|
||||
|
||||
# Изменяем размер с сохранением пропорций
|
||||
image.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# Сохраняем в байты
|
||||
output = io.BytesIO()
|
||||
image.save(output, format="JPEG", quality=quality, optimize=True)
|
||||
return output.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def validate_image(image_data: bytes) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Валидировать изображение.
|
||||
|
||||
Args:
|
||||
image_data: Байты изображения
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
try:
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
image_format = image.format
|
||||
|
||||
if image_format not in ImageProcessor.SUPPORTED_FORMATS:
|
||||
return False, f"Неподдерживаемый формат: {image_format}"
|
||||
|
||||
# Проверяем размер
|
||||
width, height = image.size
|
||||
if width > 2000 or height > 2000:
|
||||
return False, "Изображение слишком большое (максимум 2000x2000)"
|
||||
|
||||
# Проверяем файл на валидность
|
||||
image.verify()
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Ошибка валидации: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def get_image_info(image_data: bytes) -> dict:
|
||||
"""
|
||||
Получить информацию об изображении.
|
||||
|
||||
Args:
|
||||
image_data: Байты изображения
|
||||
|
||||
Returns:
|
||||
Словарь с информацией (format, size, mode)
|
||||
"""
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
return {
|
||||
"format": image.format,
|
||||
"size": image.size,
|
||||
"mode": image.mode,
|
||||
}
|
||||
|
||||
69
services/token_manager.py
Normal file
69
services/token_manager.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Управление токенами GigaChat."""
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""Менеджер токенов для GigaChat API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client_id: Optional[str] = None,
|
||||
client_secret: Optional[str] = None,
|
||||
auth_url: Optional[str] = None,
|
||||
):
|
||||
self.client_id = client_id or os.getenv("GIGACHAT_CLIENT_ID")
|
||||
self.client_secret = client_secret or os.getenv("GIGACHAT_CLIENT_SECRET")
|
||||
self.auth_url = auth_url or os.getenv(
|
||||
"GIGACHAT_AUTH_URL", "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
|
||||
)
|
||||
self._access_token: Optional[str] = None
|
||||
self._expires_at: float = 0
|
||||
|
||||
async def get_token(self, force_refresh: bool = False) -> str:
|
||||
"""
|
||||
Получить актуальный токен доступа.
|
||||
|
||||
Args:
|
||||
force_refresh: Принудительно обновить токен
|
||||
|
||||
Returns:
|
||||
Токен доступа
|
||||
"""
|
||||
if not force_refresh and self._access_token and time.time() < self._expires_at:
|
||||
return self._access_token
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
auth = aiohttp.BasicAuth(self.client_id, self.client_secret)
|
||||
async with session.post(
|
||||
self.auth_url,
|
||||
auth=auth,
|
||||
data={"scope": "GIGACHAT_API_PERS"},
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"Failed to get token: {response.status} - {error_text}")
|
||||
|
||||
data = await response.json()
|
||||
self._access_token = data["access_token"]
|
||||
# Токен обычно действителен 30 минут, обновляем за 5 минут до истечения
|
||||
expires_in = data.get("expires_in", 1800)
|
||||
self._expires_at = time.time() + expires_in - 300
|
||||
|
||||
return self._access_token
|
||||
|
||||
def is_token_valid(self) -> bool:
|
||||
"""Проверить, действителен ли текущий токен."""
|
||||
return self._access_token is not None and time.time() < self._expires_at
|
||||
|
||||
def clear_token(self):
|
||||
"""Очистить токен (для тестирования)."""
|
||||
self._access_token = None
|
||||
self._expires_at = 0
|
||||
|
||||
Reference in New Issue
Block a user