diff --git a/rag/functions/tools.py b/rag/functions/tools.py new file mode 100644 index 0000000..14e798c --- /dev/null +++ b/rag/functions/tools.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +""" +Реальные обработчики инструментов для ReAct-цикла. +Каждый обработчик — асинхронная функция, принимающая параметры и возвращающая строку с результатом. +Используются внешние API (поиск, погода и т.д.). +""" + +import json +import logging +from typing import Dict, Any, Optional +import aiohttp +import asyncio + +from rag.config_models import AppConfig + +logger = logging.getLogger(__name__) + + +class ToolHandler: + """ + Класс-контейнер для обработчиков инструментов. + Инициализируется с конфигом (для доступа к API-ключам) и HTTP-сессией. + """ + + def __init__(self, config: AppConfig): + self.config = config + self.session: Optional[aiohttp.ClientSession] = None + + async def _get_session(self) -> aiohttp.ClientSession: + if self.session is None: + self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) + return self.session + + async def close(self): + if self.session: + await self.session.close() + self.session = None + + # ------------------------------------------------------------------ + # 1. Поиск в интернете (используем DuckDuckGo Instant Answer API) + # ------------------------------------------------------------------ + async def search(self, query: str) -> str: + """ + Выполняет поиск в интернете через DuckDuckGo Instant Answer API. + Возвращает краткую выжимку или ссылки. + """ + if not query or not query.strip(): + return "❌ Пустой поисковый запрос." + + url = "https://api.duckduckgo.com/" + params = { + "q": query, + "format": "json", + "no_html": 1, + "skip_disambig": 1 + } + try: + session = await self._get_session() + async with session.get(url, params=params) as resp: + if resp.status != 200: + return f"❌ Ошибка поиска: HTTP {resp.status}" + data = await resp.json() + # Извлекаем релевантную информацию + results = [] + if data.get("AbstractText"): + results.append(f"📌 {data['AbstractText']}") + if data.get("RelatedTopics"): + for topic in data["RelatedTopics"][:3]: + if "Text" in topic: + results.append(f"• {topic['Text']}") + if not results: + return "😕 Ничего не найдено по вашему запросу." + return "\n".join(results) + except asyncio.TimeoutError: + return "⏱️ Таймаут при выполнении поиска." + except Exception as e: + logger.exception("Ошибка в search") + return f"❌ Ошибка поиска: {str(e)}" + + # ------------------------------------------------------------------ + # 2. Погода (OpenWeatherMap) + # ------------------------------------------------------------------ + async def get_weather(self, city: str) -> str: + """ + Получает текущую погоду для города через OpenWeatherMap API. + Требует API-ключ в конфиге: config.weather_api_key + """ + api_key = getattr(self.config, 'weather_api_key', None) + if not api_key: + return "❌ API-ключ для погоды не настроен (weather_api_key)." + + if not city or not city.strip(): + return "❌ Город не указан." + + url = "https://api.openweathermap.org/data/2.5/weather" + params = { + "q": city, + "appid": api_key, + "units": "metric", + "lang": "ru" + } + try: + session = await self._get_session() + async with session.get(url, params=params) as resp: + if resp.status != 200: + return f"❌ Ошибка погоды: HTTP {resp.status}" + data = await resp.json() + if data.get("cod") != 200: + return f"❌ {data.get('message', 'Неизвестная ошибка')}" + weather = data["weather"][0]["description"] + temp = data["main"]["temp"] + feels_like = data["main"]["feels_like"] + return f"🌤️ Погода в {city}: {weather}, температура {temp:.1f}°C (ощущается {feels_like:.1f}°C)" + except asyncio.TimeoutError: + return "⏱️ Таймаут при запросе погоды." + except Exception as e: + logger.exception("Ошибка в get_weather") + return f"❌ Ошибка получения погоды: {str(e)}" + + # ------------------------------------------------------------------ + # 3. Бронирование авиабилетов (заглушка — демонстрация) + # ------------------------------------------------------------------ + async def book_flight(self, origin: str, destination: str, date: str) -> str: + """ + Заглушка для бронирования авиабилетов. + В реальности здесь был бы вызов API авиакомпании или агрегатора. + """ + return (f"✈️ Заглушка бронирования: рейс {origin} → {destination} на {date}.\n" + "В реальной системе здесь был бы вызов API бронирования.") + + # ------------------------------------------------------------------ + # 4. Получение курса валют (заглушка) + # ------------------------------------------------------------------ + async def get_exchange_rate(self, from_currency: str, to_currency: str) -> str: + """ + Заглушка для получения курса валют. + Можно использовать exchangerate-api.com или аналоги. + """ + return (f"💱 Заглушка курса: 1 {from_currency} = 1.0 {to_currency} (пример)\n" + "В реальности здесь был бы вызов API курсов валют.") \ No newline at end of file