Added code samples for AI-Agents

This commit is contained in:
2025-12-17 20:22:46 +03:00
parent d66aed35d6
commit 0885618b25
29 changed files with 2007 additions and 0 deletions

13
services/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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