Compare commits
4 Commits
cba373701a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fae922931 | |||
| 9a0407e87e | |||
| 353c8ba130 | |||
| ed9f0904a9 |
47
.dockerignore
Normal file
47
.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
*.cover
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
4
.env
4
.env
@@ -1,5 +1,5 @@
|
|||||||
GIGACHAT_CLIENT_ID=your-client-id
|
GIGACHAT_CLIENT_ID=019966f0-5781-76e6-a84f-ec7de158188a
|
||||||
GIGACHAT_CLIENT_SECRET=your-client-secret
|
GIGACHAT_CLIENT_SECRET=MDE5OTY2ZjAtNTc4MS03NmU2LWE4NGYtZWM3ZGUxNTgxODhhOjI3MDMxZjIxLWY3NWYtNGI4NS05MzM1LTI4ZDYyOWM3MmM0MA==
|
||||||
GIGACHAT_AUTH_URL=https://ngw.devices.sberbank.ru:9443/api/v2/oauth
|
GIGACHAT_AUTH_URL=https://ngw.devices.sberbank.ru:9443/api/v2/oauth
|
||||||
GIGACHAT_BASE_URL=https://gigachat.devices.sberbank.ru/api/v1
|
GIGACHAT_BASE_URL=https://gigachat.devices.sberbank.ru/api/v1
|
||||||
|
|
||||||
|
|||||||
52
Dockerfile
Normal file
52
Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Многоступенчатая сборка для оптимизации размера образа
|
||||||
|
FROM python:3.11-slim as builder
|
||||||
|
|
||||||
|
# Установка системных зависимостей для сборки
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Создание виртуального окружения
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Копирование файлов зависимостей
|
||||||
|
COPY requirements.txt pyproject.toml ./
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Финальный образ
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Установка только runtime зависимостей
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Копирование виртуального окружения из builder
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Создание рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копирование кода проекта
|
||||||
|
COPY agents/ ./agents/
|
||||||
|
COPY models/ ./models/
|
||||||
|
COPY services/ ./services/
|
||||||
|
COPY prompts/ ./prompts/
|
||||||
|
COPY scripts/ ./scripts/
|
||||||
|
COPY app.py ./
|
||||||
|
|
||||||
|
# Установка переменных окружения
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONPATH=/app
|
||||||
|
|
||||||
|
# Запуск FastAPI сервера
|
||||||
|
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class ChatAgent:
|
|||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
message: str,
|
message: str,
|
||||||
conversation_id: Optional[str] = None,
|
conversation_id: Optional[str] = None,
|
||||||
model: str = "GigaChat-2-Lite",
|
model: str = "GigaChat-2",
|
||||||
) -> tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
"""
|
"""
|
||||||
Отправить сообщение и получить ответ.
|
Отправить сообщение и получить ответ.
|
||||||
@@ -39,22 +39,25 @@ class ChatAgent:
|
|||||||
context_messages = []
|
context_messages = []
|
||||||
if conversation_id:
|
if conversation_id:
|
||||||
cached_context = await self.cache.get_context(str(conversation_id))
|
cached_context = await self.cache.get_context(str(conversation_id))
|
||||||
|
# Фильтруем системные сообщения из кэша - они не должны там храниться
|
||||||
context_messages = [
|
context_messages = [
|
||||||
GigaChatMessage(role=msg["role"], content=msg["content"])
|
GigaChatMessage(role=msg["role"], content=msg["content"])
|
||||||
for msg in cached_context
|
for msg in cached_context
|
||||||
|
if msg["role"] != "system"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Добавляем системный промпт в начало
|
# Системное сообщение ВСЕГДА должно быть первым
|
||||||
system_message = GigaChatMessage(role="system", content=EARTH_PERSONA)
|
system_message = GigaChatMessage(role="system", content=EARTH_PERSONA)
|
||||||
if not context_messages or context_messages[0].role != "system":
|
# Убеждаемся, что системное сообщение первое (удаляем все системные сообщения и добавляем одно в начало)
|
||||||
|
context_messages = [msg for msg in context_messages if msg.role != "system"]
|
||||||
context_messages.insert(0, system_message)
|
context_messages.insert(0, system_message)
|
||||||
|
|
||||||
# Добавляем текущее сообщение пользователя
|
# Добавляем текущее сообщение пользователя
|
||||||
context_messages.append(GigaChatMessage(role="user", content=message))
|
context_messages.append(GigaChatMessage(role="user", content=message))
|
||||||
|
|
||||||
# Отправляем запрос
|
# Отправляем запрос (не передаем message отдельно, т.к. оно уже в context_messages)
|
||||||
response = await self.gigachat.chat_with_response(
|
response = await self.gigachat.chat_with_response(
|
||||||
message=message,
|
message="", # Пустое, т.к. сообщение уже добавлено в context_messages
|
||||||
context=context_messages,
|
context=context_messages,
|
||||||
model=model,
|
model=model,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
@@ -76,7 +79,7 @@ class ChatAgent:
|
|||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
message: str,
|
message: str,
|
||||||
context: Optional[List[dict]] = None,
|
context: Optional[List[dict]] = None,
|
||||||
model: str = "GigaChat-2-Lite",
|
model: str = "GigaChat-2",
|
||||||
) -> tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
"""
|
"""
|
||||||
Отправить сообщение с явным контекстом.
|
Отправить сообщение с явным контекстом.
|
||||||
@@ -100,8 +103,9 @@ class ChatAgent:
|
|||||||
|
|
||||||
context_messages.append(GigaChatMessage(role="user", content=message))
|
context_messages.append(GigaChatMessage(role="user", content=message))
|
||||||
|
|
||||||
|
# Отправляем запрос (не передаем message отдельно, т.к. оно уже в context_messages)
|
||||||
response = await self.gigachat.chat_with_response(
|
response = await self.gigachat.chat_with_response(
|
||||||
message=message,
|
message="", # Пустое, т.к. сообщение уже добавлено в context_messages
|
||||||
context=context_messages,
|
context=context_messages,
|
||||||
model=model,
|
model=model,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class GigaChatClient:
|
|||||||
async def _get_session(self) -> aiohttp.ClientSession:
|
async def _get_session(self) -> aiohttp.ClientSession:
|
||||||
"""Получить HTTP сессию (lazy initialization)."""
|
"""Получить HTTP сессию (lazy initialization)."""
|
||||||
if self._session is None or self._session.closed:
|
if self._session is None or self._session.closed:
|
||||||
self._session = aiohttp.ClientSession()
|
connector = aiohttp.TCPConnector(ssl=False)
|
||||||
|
self._session = aiohttp.ClientSession(connector=connector)
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
async def chat(
|
async def chat(
|
||||||
@@ -73,7 +74,7 @@ class GigaChatClient:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: Текст сообщения
|
message: Текст сообщения
|
||||||
context: История сообщений
|
context: История сообщений (уже должна содержать системное сообщение первым)
|
||||||
model: Модель GigaChat
|
model: Модель GigaChat
|
||||||
temperature: Температура генерации
|
temperature: Температура генерации
|
||||||
max_tokens: Максимальное количество токенов
|
max_tokens: Максимальное количество токенов
|
||||||
@@ -81,7 +82,21 @@ class GigaChatClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Полный ответ от API
|
Полный ответ от API
|
||||||
"""
|
"""
|
||||||
messages = context or []
|
# Создаем копию списка, чтобы не изменять оригинал
|
||||||
|
messages = list(context) if context else []
|
||||||
|
|
||||||
|
# Убеждаемся, что системное сообщение первое
|
||||||
|
system_messages = [msg for msg in messages if msg.role == "system"]
|
||||||
|
non_system_messages = [msg for msg in messages if msg.role != "system"]
|
||||||
|
|
||||||
|
# Если есть системные сообщения, берем первое, иначе оставляем список пустым
|
||||||
|
if system_messages:
|
||||||
|
messages = [system_messages[0]] + non_system_messages
|
||||||
|
else:
|
||||||
|
messages = non_system_messages
|
||||||
|
|
||||||
|
# Добавляем текущее сообщение пользователя только если его еще нет в конце
|
||||||
|
if not messages or messages[-1].role != "user" or messages[-1].content != message:
|
||||||
messages.append(GigaChatMessage(role="user", content=message))
|
messages.append(GigaChatMessage(role="user", content=message))
|
||||||
|
|
||||||
request = GigaChatRequest(
|
request = GigaChatRequest(
|
||||||
|
|||||||
143
app.py
Normal file
143
app.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"""FastAPI сервер для AI-агентов."""
|
||||||
|
import os
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, APIRouter
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from agents.gigachat_client import GigaChatClient
|
||||||
|
from agents.chat_agent import ChatAgent
|
||||||
|
from services.token_manager import TokenManager
|
||||||
|
from services.cache_service import CacheService
|
||||||
|
from models.gigachat_types import GigaChatMessage
|
||||||
|
|
||||||
|
app = FastAPI(title="New Planet AI Agents API", version="1.0.0")
|
||||||
|
|
||||||
|
# CORS middleware для работы с backend
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # В production указать конкретные домены
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Роутер для API эндпоинтов
|
||||||
|
api_router = APIRouter(prefix="/api/v1", tags=["ai"])
|
||||||
|
|
||||||
|
# Инициализация сервисов
|
||||||
|
token_manager = TokenManager()
|
||||||
|
gigachat_client = GigaChatClient(token_manager)
|
||||||
|
cache_service = CacheService() # Использует REDIS_URL из окружения
|
||||||
|
chat_agent = ChatAgent(gigachat_client, cache_service)
|
||||||
|
|
||||||
|
|
||||||
|
# Модели запросов/ответов
|
||||||
|
class ChatRequest(BaseModel):
|
||||||
|
"""Запрос на отправку сообщения в чат."""
|
||||||
|
message: str = Field(..., min_length=1, max_length=2000)
|
||||||
|
conversation_id: Optional[str] = None
|
||||||
|
context: Optional[List[Dict[str, Any]]] = None
|
||||||
|
model: Optional[str] = "GigaChat-2"
|
||||||
|
user_id: Optional[UUID] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ChatResponse(BaseModel):
|
||||||
|
"""Ответ от ИИ-агента."""
|
||||||
|
response: str
|
||||||
|
conversation_id: Optional[str] = None
|
||||||
|
tokens_used: Optional[int] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateTextRequest(BaseModel):
|
||||||
|
"""Запрос на генерацию текста."""
|
||||||
|
prompt: str = Field(..., min_length=1)
|
||||||
|
model: Optional[str] = "GigaChat-2-Pro"
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateTextResponse(BaseModel):
|
||||||
|
"""Ответ с сгенерированным текстом."""
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Проверка здоровья сервиса."""
|
||||||
|
return {"status": "ok", "service": "ai-agents"}
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/chat", response_model=ChatResponse)
|
||||||
|
async def chat(request: ChatRequest):
|
||||||
|
"""
|
||||||
|
Отправить сообщение в чат через ChatAgent.
|
||||||
|
|
||||||
|
Поддерживает два режима:
|
||||||
|
1. С conversation_id - использует ChatAgent с контекстом из Redis
|
||||||
|
2. С явным context - использует chat_with_context
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_id = request.user_id or uuid4()
|
||||||
|
|
||||||
|
if request.context:
|
||||||
|
# Используем явный контекст
|
||||||
|
response_text, tokens_used = await chat_agent.chat_with_context(
|
||||||
|
user_id=user_id,
|
||||||
|
message=request.message,
|
||||||
|
context=request.context,
|
||||||
|
model=request.model or "GigaChat-2",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Используем ChatAgent с conversation_id
|
||||||
|
response_text, tokens_used = await chat_agent.chat(
|
||||||
|
user_id=user_id,
|
||||||
|
message=request.message,
|
||||||
|
conversation_id=request.conversation_id,
|
||||||
|
model=request.model or "GigaChat-2",
|
||||||
|
)
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
response=response_text,
|
||||||
|
conversation_id=request.conversation_id,
|
||||||
|
tokens_used=tokens_used,
|
||||||
|
model=request.model or "GigaChat-2",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/generate_text", response_model=GenerateTextResponse)
|
||||||
|
async def generate_text(request: GenerateTextRequest):
|
||||||
|
"""
|
||||||
|
Генерация текста по промпту через GigaChat.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response_text = await gigachat_client.chat(
|
||||||
|
message=request.prompt,
|
||||||
|
model=request.model or "GigaChat-2-Pro",
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
return GenerateTextResponse(text=response_text)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Generate text error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# Подключение роутера к приложению
|
||||||
|
app.include_router(api_router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
"""Закрытие соединений при остановке."""
|
||||||
|
await gigachat_client.close()
|
||||||
|
await cache_service.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
ai-agents:
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: new-planet-ai-agents
|
||||||
|
environment:
|
||||||
|
- REDIS_URL=redis://newplanet-redis:6379/0
|
||||||
|
- GIGACHAT_CLIENT_ID=${GIGACHAT_CLIENT_ID}
|
||||||
|
- GIGACHAT_CLIENT_SECRET=${GIGACHAT_CLIENT_SECRET}
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
|
- PYTHONPATH=/app
|
||||||
|
volumes:
|
||||||
|
- ./agents:/app/agents
|
||||||
|
- ./models:/app/models
|
||||||
|
- ./services:/app/services
|
||||||
|
- ./prompts:/app/prompts
|
||||||
|
- ./scripts:/app/scripts
|
||||||
|
- ./app.py:/app/app.py
|
||||||
|
networks:
|
||||||
|
- new-planet-network
|
||||||
|
# Запуск FastAPI сервера
|
||||||
|
command: uvicorn app:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
networks:
|
||||||
|
new-planet-network:
|
||||||
|
external: true
|
||||||
@@ -41,9 +41,9 @@ class GigaChatUsage(BaseModel):
|
|||||||
class GigaChatResponse(BaseModel):
|
class GigaChatResponse(BaseModel):
|
||||||
"""Ответ от GigaChat API."""
|
"""Ответ от GigaChat API."""
|
||||||
|
|
||||||
id: str
|
id: Optional[str] = None
|
||||||
object: str
|
object: Optional[str] = None
|
||||||
created: int
|
created: Optional[int] = None
|
||||||
model: str
|
model: str
|
||||||
choices: List[GigaChatChoice]
|
choices: List[GigaChatChoice]
|
||||||
usage: GigaChatUsage
|
usage: GigaChatUsage
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Pydantic модели для расписаний."""
|
"""Pydantic модели для расписаний."""
|
||||||
from datetime import date
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date as date_type
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ class Schedule(BaseModel):
|
|||||||
|
|
||||||
id: Optional[UUID] = None
|
id: Optional[UUID] = None
|
||||||
title: str = Field(..., description="Название расписания")
|
title: str = Field(..., description="Название расписания")
|
||||||
date: date = Field(..., description="Дата расписания")
|
date: date_type = Field(..., description="Дата расписания")
|
||||||
tasks: List[Task] = Field(default_factory=list, description="Список заданий")
|
tasks: List[Task] = Field(default_factory=list, description="Список заданий")
|
||||||
user_id: Optional[UUID] = None
|
user_id: Optional[UUID] = None
|
||||||
created_at: Optional[str] = None
|
created_at: Optional[str] = None
|
||||||
@@ -35,6 +37,6 @@ class ScheduleGenerateRequest(BaseModel):
|
|||||||
|
|
||||||
child_age: int = Field(..., ge=1, le=18, description="Возраст ребенка")
|
child_age: int = Field(..., ge=1, le=18, description="Возраст ребенка")
|
||||||
preferences: List[str] = Field(default_factory=list, description="Предпочтения ребенка")
|
preferences: List[str] = Field(default_factory=list, description="Предпочтения ребенка")
|
||||||
date: date = Field(..., description="Дата расписания")
|
date: date_type = Field(..., description="Дата расписания")
|
||||||
existing_tasks: Optional[List[str]] = Field(None, description="Существующие задания для учета")
|
existing_tasks: Optional[List[str]] = Field(None, description="Существующие задания для учета")
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ CHAT_SYSTEM_PROMPT = """Ты планета Земля - помощник для
|
|||||||
- Помогать понять задания
|
- Помогать понять задания
|
||||||
- Мотивировать и поддерживать
|
- Мотивировать и поддерживать
|
||||||
- Объяснять простым языком
|
- Объяснять простым языком
|
||||||
|
- Расписывать действия пошагово
|
||||||
|
|
||||||
Правила общения:
|
Правила общения:
|
||||||
- Используй короткие предложения
|
- Используй короткие предложения
|
||||||
|
|||||||
@@ -37,18 +37,37 @@ SCHEDULE_GENERATION_PROMPT = """Ты планета Земля, друг дет
|
|||||||
Категории заданий: утренняя_рутина, обучение, игра, отдых, вечерняя_рутина
|
Категории заданий: утренняя_рутина, обучение, игра, отдых, вечерняя_рутина
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SCHEDULE_UPDATE_PROMPT = """Ты планета Земля. Обнови расписание с учетом следующих изменений:
|
SCHEDULE_UPDATE_PROMPT = """Ты планета Земля, друг детей с расстройством аутистического спектра (РАС).
|
||||||
|
|
||||||
Существующее расписание:
|
Текущее расписание:
|
||||||
{existing_schedule}
|
{existing_schedule}
|
||||||
|
|
||||||
Запрос пользователя:
|
Запрос пользователя: {user_request}
|
||||||
{user_request}
|
|
||||||
|
|
||||||
Верни ТОЛЬКО валидный JSON с обновленным расписанием:
|
Обнови расписание согласно запросу. Сохрани структуру и логику расписания, но внеси необходимые изменения.
|
||||||
|
|
||||||
|
Важные правила при обновлении:
|
||||||
|
1. Сохраняй простоту и понятность заданий
|
||||||
|
2. Поддерживай четкие временные рамки
|
||||||
|
3. Избегай резких переходов между активностями
|
||||||
|
4. Включи время на отдых между заданиями
|
||||||
|
5. Учитывай особенности РАС
|
||||||
|
|
||||||
|
Верни ТОЛЬКО валидный JSON формат без дополнительного текста:
|
||||||
{{
|
{{
|
||||||
"title": "Название расписания",
|
"title": "Название расписания",
|
||||||
"tasks": [...]
|
"description": "Краткое описание",
|
||||||
|
"tasks": [
|
||||||
|
{{
|
||||||
|
"title": "Название задания",
|
||||||
|
"description": "Подробное описание",
|
||||||
|
"duration_minutes": 30,
|
||||||
|
"category": "утренняя_рутина",
|
||||||
|
"order": 0
|
||||||
}}
|
}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
|
||||||
|
Категории заданий: утренняя_рутина, обучение, игра, отдых, вечерняя_рутина
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ numpy
|
|||||||
aiohttp
|
aiohttp
|
||||||
redis
|
redis
|
||||||
Pillow
|
Pillow
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
|
||||||
# Dev
|
# Dev
|
||||||
pytest
|
pytest
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class CacheService:
|
|||||||
"""Сервис для работы с Redis кэшем."""
|
"""Сервис для работы с Redis кэшем."""
|
||||||
|
|
||||||
def __init__(self, redis_url: Optional[str] = None):
|
def __init__(self, redis_url: Optional[str] = None):
|
||||||
self.redis_url = redis_url or "redis://localhost:6379/0"
|
import os
|
||||||
|
self.redis_url = redis_url or os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||||
self._client: Optional[redis.Redis] = None
|
self._client: Optional[redis.Redis] = None
|
||||||
|
|
||||||
async def _get_client(self) -> redis.Redis:
|
async def _get_client(self) -> redis.Redis:
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
"""Управление токенами GigaChat."""
|
"""Управление токенами GigaChat."""
|
||||||
|
import base64
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from aiohttp import FormData
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -17,10 +21,13 @@ class TokenManager:
|
|||||||
client_id: Optional[str] = None,
|
client_id: Optional[str] = None,
|
||||||
client_secret: Optional[str] = None,
|
client_secret: Optional[str] = None,
|
||||||
auth_url: Optional[str] = None,
|
auth_url: Optional[str] = None,
|
||||||
|
credentials: Optional[str] = None,
|
||||||
):
|
):
|
||||||
self.client_id = client_id or os.getenv("GIGACHAT_CLIENT_ID")
|
# Приоритет: переданные параметры > переменные окружения > .env файл
|
||||||
self.client_secret = client_secret or os.getenv("GIGACHAT_CLIENT_SECRET")
|
self.credentials = credentials or os.environ.get("GIGACHAT_CREDENTIALS") or os.getenv("GIGACHAT_CREDENTIALS")
|
||||||
self.auth_url = auth_url or os.getenv(
|
self.client_id = client_id or os.environ.get("GIGACHAT_CLIENT_ID") or os.getenv("GIGACHAT_CLIENT_ID")
|
||||||
|
self.client_secret = client_secret or os.environ.get("GIGACHAT_CLIENT_SECRET") or os.getenv("GIGACHAT_CLIENT_SECRET")
|
||||||
|
self.auth_url = auth_url or os.environ.get("GIGACHAT_AUTH_URL") or os.getenv(
|
||||||
"GIGACHAT_AUTH_URL", "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
|
"GIGACHAT_AUTH_URL", "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
|
||||||
)
|
)
|
||||||
self._access_token: Optional[str] = None
|
self._access_token: Optional[str] = None
|
||||||
@@ -39,12 +46,31 @@ class TokenManager:
|
|||||||
if not force_refresh and self._access_token and time.time() < self._expires_at:
|
if not force_refresh and self._access_token and time.time() < self._expires_at:
|
||||||
return self._access_token
|
return self._access_token
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
# Определяем, какой вариант используется: готовый ключ или client_id/client_secret
|
||||||
auth = aiohttp.BasicAuth(self.client_id, self.client_secret)
|
if self.credentials:
|
||||||
|
# Используем готовый ключ авторизации (уже закодированный в Base64)
|
||||||
|
# Убираем префикс "Basic " если он есть
|
||||||
|
credentials_key = self.credentials.strip().replace('\n', '').replace('\r', '')
|
||||||
|
if credentials_key.startswith('Basic '):
|
||||||
|
credentials_key = credentials_key[6:]
|
||||||
|
|
||||||
|
connector = aiohttp.TCPConnector(ssl=False)
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Basic {credentials_key}",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"RqUID": str(uuid.uuid4())
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"scope": "GIGACHAT_API_PERS"
|
||||||
|
}
|
||||||
|
|
||||||
async with session.post(
|
async with session.post(
|
||||||
self.auth_url,
|
self.auth_url,
|
||||||
auth=auth,
|
headers=headers,
|
||||||
data={"scope": "GIGACHAT_API_PERS"},
|
data=form_data,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error_text = await response.text()
|
error_text = await response.text()
|
||||||
@@ -52,11 +78,69 @@ class TokenManager:
|
|||||||
|
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
self._access_token = data["access_token"]
|
self._access_token = data["access_token"]
|
||||||
# Токен обычно действителен 30 минут, обновляем за 5 минут до истечения
|
|
||||||
expires_in = data.get("expires_in", 1800)
|
expires_in = data.get("expires_in", 1800)
|
||||||
self._expires_at = time.time() + expires_in - 300
|
self._expires_at = time.time() + expires_in - 300
|
||||||
|
|
||||||
return self._access_token
|
return self._access_token
|
||||||
|
elif self.client_id and self.client_secret:
|
||||||
|
# Очищаем от пробелов и переносов
|
||||||
|
client_secret = self.client_secret.strip().replace('\n', '').replace('\r', '')
|
||||||
|
|
||||||
|
# Проверяем, является ли client_secret уже закодированным ключом Base64
|
||||||
|
# Если secret начинается с букв/цифр и длиннее 50 символов, это уже ключ авторизации
|
||||||
|
is_already_encoded = len(client_secret) > 50 and all(c.isalnum() or c in '+/=' for c in client_secret)
|
||||||
|
|
||||||
|
if is_already_encoded:
|
||||||
|
# Это уже готовый ключ авторизации в Base64
|
||||||
|
encoded_credentials = client_secret
|
||||||
|
print(f"DEBUG: Using pre-encoded authorization key (length: {len(encoded_credentials)})")
|
||||||
|
else:
|
||||||
|
# Это настоящий client_id и client_secret, нужно закодировать
|
||||||
|
client_id = self.client_id.strip().replace('\n', '').replace('\r', '')
|
||||||
|
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise Exception("GIGACHAT_CLIENT_ID and GIGACHAT_CLIENT_SECRET cannot be empty after cleaning")
|
||||||
|
|
||||||
|
credentials_string = f"{client_id}:{client_secret}"
|
||||||
|
encoded_credentials = base64.b64encode(credentials_string.encode('utf-8')).decode('utf-8')
|
||||||
|
print(f"DEBUG: Encoded client_id:client_secret (length: {len(encoded_credentials)})")
|
||||||
|
|
||||||
|
connector = aiohttp.TCPConnector(ssl=False)
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"RqUID": str(uuid.uuid4()),
|
||||||
|
"Authorization": f"Basic {encoded_credentials}"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"scope": "GIGACHAT_API_PERS"}
|
||||||
|
|
||||||
|
print(f"DEBUG: Authorization header starts with: Basic {encoded_credentials[:10]}...")
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
self.auth_url,
|
||||||
|
headers=headers,
|
||||||
|
data=payload,
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(
|
||||||
|
f"Failed to get token: {response.status} - {error_text}. "
|
||||||
|
f"URL: {self.auth_url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = await response.json()
|
||||||
|
self._access_token = data["access_token"]
|
||||||
|
expires_in = data.get("expires_in", 1800)
|
||||||
|
self._expires_at = time.time() + expires_in - 300
|
||||||
|
|
||||||
|
return self._access_token
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"Either GIGACHAT_CREDENTIALS (ready authorization key) or "
|
||||||
|
"GIGACHAT_CLIENT_ID and GIGACHAT_CLIENT_SECRET must be set"
|
||||||
|
)
|
||||||
|
|
||||||
def is_token_valid(self) -> bool:
|
def is_token_valid(self) -> bool:
|
||||||
"""Проверить, действителен ли текущий токен."""
|
"""Проверить, действителен ли текущий токен."""
|
||||||
|
|||||||
Reference in New Issue
Block a user