diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/cloud_instruments/ai_assistants.py b/cloud_instruments/ai_assistants.py new file mode 100644 index 0000000..5fd8330 --- /dev/null +++ b/cloud_instruments/ai_assistants.py @@ -0,0 +1,62 @@ +import os +import json +from dotenv import load_dotenv +from loguru import logger +from yandex_cloud_ml_sdk import YCloudML + +# Загрузка переменных окружения из файла .env +load_dotenv() + +# Получение идентификаторов папки и API ключа из переменных окружения +FOLDER_ID = os.getenv('YC_FOLDER_ID') +API_KEY = os.getenv('YC_API_KEY') + +# Инициализация SDK Yandex Cloud с использованием полученных идентификаторов +sdk = YCloudML(folder_id=FOLDER_ID, auth=API_KEY) + +def create_assistant(): + """Создает ассистента с заданными параметрами.""" + # Открываем файл index_id.json для получения идентификатора индекса + with open('index_id.json', 'r') as file: + data = json.load(file) + index_id = data.get('index_id') # Извлекаем index_id из данных + + # Получаем индекс поиска по его идентификатору + search_index = sdk.search_indexes.get(index_id) + # Создаем инструмент поиска с максимальным количеством результатов 5 + search_tool = sdk.tools.search_index(search_index, max_num_results=5) + + # Создаем ассистента с заданными параметрами + return sdk.assistants.create( + name="foo-assistant", # Имя ассистента + model='yandexgpt', # Модель, которую будет использовать ассистент + temperature=0.1, # Параметр, определяющий креативность ответов + instruction="Вы ассистируете пользователю в Telegram. Отвечайте на вопросы, которые он задает. Игнорируйте контекст, если считаете его нерелевантным.", # Инструкция для ассистента + tools=[search_tool], # Инструменты, которые будет использовать ассистент + ttl_days=30, # Время жизни ассистента в днях + expiration_policy="SINCE_LAST_ACTIVE" # Политика истечения + ) + +# Создаем ассистента и сохраняем его в переменной +assistant = create_assistant() + +async def ai_assistant(message: str, thread_id: str): + """Отправляет сообщение в указанный поток и получает ответ ассистента.""" + # Получаем поток по его идентификатору + thread = sdk.threads.get(thread_id) + # Записываем сообщение в поток + thread.write(message) + + # Запускаем ассистента и ждем ответа + response = assistant.run(thread.id).wait() + return response.message.parts[0] # Возвращаем первый элемент ответа + +async def ai_assistant_new_thread(chat_id: str) -> str: + """Создает новый поток для чата.""" + # Создаем новый поток с заданным именем и временем жизни 7 дней + thread = sdk.threads.create( + name=f'thread-{chat_id}', # Имя потока, основанное на идентификаторе чата + ttl_days=7, # Время жизни потока в днях + expiration_policy="SINCE_LAST_ACTIVE" # Политика истечения + ) + return thread.id # Возвращаем идентификатор нового потока \ No newline at end of file diff --git a/cloud_instruments/create_rag_index.py b/cloud_instruments/create_rag_index.py new file mode 100644 index 0000000..2612ee1 --- /dev/null +++ b/cloud_instruments/create_rag_index.py @@ -0,0 +1,64 @@ +import os +import json +from dotenv import load_dotenv +from loguru import logger +from yandex_cloud_ml_sdk import YCloudML +from yandex_cloud_ml_sdk.search_indexes import VectorSearchIndexType, StaticIndexChunkingStrategy + +# Загрузка переменных окружения +load_dotenv() + +# Получение идентификатора папки и API ключа из переменных окружения +FOLDER_ID = os.getenv("YC_FOLDER_ID") +YANDEX_API_KEY = os.getenv("YC_API_KEY") +DATA_DIR = os.getenv("DATA_DIR") + +# Инициализация SDK с авторизацией +sdk = YCloudML(folder_id=FOLDER_ID, auth=YANDEX_API_KEY) + +# Путь к директории с данными +data_directory = f"knowledge/{DATA_DIR}" + +# Список для хранения ссылок на загруженные файлы +uploaded_files = [] + +# Загрузка всех файлов из директории данных +for filename in os.listdir(data_directory): + file_path = os.path.join(data_directory, filename) + if os.path.isfile(file_path): + uploaded_file = sdk.files.upload( + file_path, + name=filename, + description=f"Данные базы знаний из {data_directory}", + ttl_days=30, + expiration_policy="SINCE_LAST_ACTIVE" + ) + uploaded_files.append(uploaded_file) + +# Создание типа векторного поискового индекса +index_type = VectorSearchIndexType( + chunking_strategy=StaticIndexChunkingStrategy( + max_chunk_size_tokens=1000, + chunk_overlap_tokens=200 + ), + doc_embedder_uri=f"emb://{FOLDER_ID}/text-search-doc/rc", + query_embedder_uri=f"emb://{FOLDER_ID}/text-search-query/rc" +) + +# Создание поискового индекса с загруженными файлами +operation = sdk.search_indexes.create_deferred( + files=uploaded_files, + index_type=index_type, + name="rag_search_index", + description=f"Данные базы знаний из {data_directory}", + ttl_days=30, + expiration_policy="SINCE_LAST_ACTIVE" +) + +# Ожидание завершения создания поискового индекса +index = operation.wait() + +# Сохранение идентификатора индекса в JSON файл +index_id = {"index_id": index.id} +with open("index_id.json", "w") as json_file: + json.dump(index_id, json_file, indent=4) \ No newline at end of file diff --git a/cloud_instruments/searchapi.py b/cloud_instruments/searchapi.py new file mode 100644 index 0000000..291f2e7 --- /dev/null +++ b/cloud_instruments/searchapi.py @@ -0,0 +1,85 @@ +import os +import requests +import dotenv +from loguru import logger +from yandex_cloud_ml_sdk import YCloudML + +# Загрузка переменных окружения +dotenv.load_dotenv() + +# Получение идентификаторов папки и API ключа +FOLDER_ID = os.getenv('YC_FOLDER_ID') +API_KEY = os.getenv('YC_API_KEY') + +# URL для генеративного поиска +SEARCH_API_GENERATIVE = f"https://ya.ru/search/xml/generative?folderid={FOLDER_ID}" +SERP_SITE = os.getenv('SERP_SITE', None) +SERP_HOST = os.getenv('SERP_HOST', None) +SERP_URL = os.getenv('SERP_URL', None) + +# Инициализация SDK Yandex Cloud +sdk = YCloudML(folder_id=FOLDER_ID, auth=API_KEY) + +def process_response(response): + """Обрабатывает ответ от API и возвращает комбинированный контент.""" + content = "" + sources = [] + + # Проверяем тип контента в ответе + if "application/json" in response.headers.get("Content-Type", ""): + content = response.json().get("message", {}).get("content", "") + sources = response.json().get("links", []) + logger.info(content) + for i, link in enumerate(sources, start=1): + logger.info(f"[{i}]: {link}") + elif "text/xml" in response.headers.get("Content-Type", ""): + logger.error(f"Ошибка: {response.text}") + else: + logger.error(f"Неожиданный тип контента: {response.text}") + + # Формируем комбинированный контент для ответа + combined_content = f"Ответ SearchAPI:\n{content}\n\nИсточники:\n" + "\n".join(sources) + return combined_content + +async def search_api_generative_contextual(message: str, thread_id: str): + """Выполняет генеративный поиск с учетом контекста треда.""" + # Получаем сообщения из треда + thread_messages = sdk.threads.get(thread_id).read() + messages = [{"content": item.parts[0], "role": item.role} for item in thread_messages] + + # Добавляем новое сообщение от пользователя + messages.append({"content": message, "role": "user"}) + + headers = {"Authorization": f"Api-Key {API_KEY}"} + data = { + "messages": messages, + "site": SERP_SITE, + "host": SERP_HOST, + "url": SERP_URL + } + + # Отправляем запрос к API + response = requests.post(SEARCH_API_GENERATIVE, headers=headers, json=data) + combined_content = process_response(response) + + # Записываем сообщения в тред + thread = sdk.threads.get(thread_id) + thread.write(message) + thread.write(combined_content, labels={"role": "assistant"}) + return combined_content + +async def search_api_generative(message: str): + """Выполняет генеративный поиск без контекста треда.""" + headers = {"Authorization": f"Api-Key {API_KEY}"} + data = { + "messages": [{"content": message, "role": "user"}], + "site": SERP_SITE, + "host": SERP_HOST, + "url": SERP_URL + } + + # Отправляем запрос к API + response = requests.post(SEARCH_API_GENERATIVE, headers=headers, json=data) + combined_content = process_response(response) + + return combined_content \ No newline at end of file diff --git a/cloud_instruments/sf.py b/cloud_instruments/sf.py new file mode 100644 index 0000000..d07ccbb --- /dev/null +++ b/cloud_instruments/sf.py @@ -0,0 +1,13 @@ +import httpx + +async def send_post_request(name): + url = f"https://functions.yandexcloud.net/d4ekuoccv9lmm79dh5rr?name={name}" + async with httpx.AsyncClient() as client: + response = await client.get(url) + print(f"Status Code: {response.status_code}") + print(f"Response Content: {response.text}") + +if __name__ == "__main__": + import asyncio + name = "Master" + asyncio.run(send_post_request(name)) \ No newline at end of file diff --git a/data/example.db b/data/example.db new file mode 100644 index 0000000..5c51986 Binary files /dev/null and b/data/example.db differ diff --git a/fc_guide.py b/fc_guide.py new file mode 100644 index 0000000..e69de29 diff --git a/fc_sqlite.py b/fc_sqlite.py new file mode 100644 index 0000000..e69de29 diff --git a/fc_sqlite_init.py b/fc_sqlite_init.py new file mode 100644 index 0000000..c01fb5e --- /dev/null +++ b/fc_sqlite_init.py @@ -0,0 +1,36 @@ +import sqlite3 + +# Создаем соединение с базой данных +conn = sqlite3.connect('data/example.db') +cursor = conn.cursor() + +# Создаем таблицу products +cursor.execute(''' +CREATE TABLE IF NOT EXISTS products ( + productId TEXT PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, + price REAL NOT NULL, + num_of_orders INTEGER NOT NULL, + rating REAL NOT NULL +) +''') + +# Заполняем таблицу 5 товарами категории электроника +products = [ + ('1', 'Смартфон XYZ', 'электроника', 19999.99, 150, 4.5), + ('2', 'Ноутбук ABC', 'электроника', 59999.99, 75, 4.7), + ('3', 'Наушники DEF', 'электроника', 2999.99, 200, 4.2), + ('4', 'Телевизор GHI', 'электроника', 49999.99, 50, 4.6), + ('5', 'Планшет JKL', 'электроника', 24999.99, 100, 4.3), + ('6', 'Арбуз', 'ягоды', 100, 5000000, 5), +] + +cursor.executemany(''' +INSERT INTO products (productId, name, category, price, num_of_orders, rating) +VALUES (?, ?, ?, ?, ?, ?) +''', products) + +# Сохраняем изменения и закрываем соединение +conn.commit() +conn.close() diff --git a/fc_utils.py b/fc_utils.py new file mode 100644 index 0000000..c822c2d --- /dev/null +++ b/fc_utils.py @@ -0,0 +1,326 @@ +import httpx +import time +from tenacity import retry, wait_random_exponential, stop_after_attempt +import os +import sqlite3 +from termcolor import colored + +GPT_MODEL = "yandexgpt/rc" +FOLDER_ID = os.getenv('FOLDER_ID') or '' +API_KEY = os.getenv('YANDEX_API_KEY') or '' + +db_path = 'data/example.db' + +products_table_description = """ +Таблица "products" предназначена для хранения информации о товарах, доступных в магазине. +Каждый товар имеет следующие поля: +- productId: уникальный идентификатор товара (TEXT, PRIMARY KEY) +- name: название товара (TEXT, NOT NULL) +- category: категория товара (TEXT, NOT NULL). Например: электроника, одежда, книги, дом, ягоды, сад +- price: цена товара (REAL, NOT NULL) +- num_of_orders: количество заказов данного товара (INTEGER, NOT NULL) +- rating: рейтинг товара (REAL, NOT NULL) +""" + +@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3)) +def chat_completion_request(messages, tools=None, model=GPT_MODEL): + payload = { + "modelUri": f"gpt://{FOLDER_ID}/{model}", + "completionOptions": { + "stream": False, + "temperature": 0, + "maxTokens": 1000 + }, + "messages": messages, + "tools": tools + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Api-Key {API_KEY}" + } + + url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion" + try: + response = httpx.post( + url=url, + json=payload, + headers=headers, + timeout=60.0 + ) + + response_data = response.json() + + if response.status_code != 200: + raise Exception(f"API error: {response.status_code}, {response_data}") + + return response_data + + except Exception as e: + print(f"Error occurred: {e}") + raise e + +def get_function_call_from_response(response): + return response['result']['alternatives'][0]['message'].get('toolCallList', {}).get('toolCalls', []) + +def pack_function_result(name: str, content: str): + return { + "functionResult": { + "name": name, + "content": str(content) + } + } + +def process_tool_product_list_tool(category: str, priceMinMax: list[int], sortBy: str = 'price') -> list[dict]: + return [{'productId': 'siaomi453', 'name': 'сяоми 4+ pro', 'category': category, 'price': priceMinMax[1]}] + +def process_tool_balance_tool(userId: str) -> dict: + # Обработка аргументов: userId + return {'userId': userId, 'balance': 10000} + +def process_tool_order_tool(productId: str, quantity: int) -> dict: + # Обработка аргументов: productId, quantity + return {'orderId': 'order123', 'productId': productId, 'quantity': quantity} + +def process_tool_sql_tool(query: str) -> str: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute(query) + result = cursor.fetchall() + conn.close() + return result + +def process_functions(toolCalls): + tools_map = { + "ProductListTool": process_tool_product_list_tool, + "BalanceTool": process_tool_balance_tool, + "OrderTool": process_tool_order_tool, + "ask_database": process_tool_sql_tool + } + + results = [] + for tool_call in toolCalls: + name = tool_call['functionCall']['name'] + arguments = tool_call['functionCall'].get('arguments', {}) + if name in tools_map: + # Вызываем функцию с переданными аргументами + result = tools_map[name](**arguments) + + results.append(pack_function_result(name, result)) + + return results + +def pretty_print_conversation(messages): + role_to_color = { + "system": "red", + "user": "green", + "assistant": "blue", + "function": "magenta", + } + + for message in messages: + if message["role"] == "system": + print(colored(f"system: {message['text']}\n", role_to_color[message["role"]])) + elif message["role"] == "user": + print(colored(f"user: {message['text']}\n", role_to_color[message["role"]])) + elif message["role"] == "assistant" and (message.get("toolCallList") or message.get("toolResultList")): + print(colored(f"assistant: {message.get('toolCallList') or message.get('toolResultList')}\n", role_to_color["function"])) + elif message["role"] == "assistant" and not (message.get("toolCallList") or message.get("toolResultList")): + print(colored(f"assistant: {message['text']}\n", role_to_color[message["role"]])) + + +if __name__ == '__main__': + test_messages = [ + {"role": "system", "text": "Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. "}, + {"role": "user", "text": "Сколько у меня денег на балансе?"} + ] + + test_messages = [ + {"role": "system", "text": "Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. Если пользователь ищет товары, то всегда параллельно с вызовом инструмента ProductListTool вызывай и BalanceTool."}, + {"role": "user", "text": "Покажи мне товары категории 'электроника' в диапазоне цен от 1000 до 5000 рублей, отсортированные по популярности."} + ] + + test_messages = [ + {"role": "system", "text": "Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда"}, + {"role": "user", "text": f"Мне нужны все товары категории 'электроника', отсортированные по рейтингу, ценой меньше 30к."} + ] + + weather_tool = { + "function": { + "name": "WeatherTool", + "description": "Ходит в API и получает погоду в городе", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "Название города, для которого нужно получить погоду." + }, + "units": { + "type": "string", + "enum": ["metric", "imperial"], + "default": "metric", + "description": "Единицы измерения температуры. 'metric' для Цельсия, 'imperial' для Фаренгейта." + }, + "region": { + "type": "string", + "default": "center", + "description": "Часть города, для которой нужно получить погоду." + } + }, + "required": ["city"], + "additionalProperties": False + } + } + } + + product_list_tool = { + "function": { + "name": "ProductListTool", + "description": "Получает список товаров из базы данных на основе заданных параметров. Позволяет фильтровать товары по диапазону цен, категории и критерию сортировки.", + "parameters": { + "type": "object", + "properties": { + "priceMinMax": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Диапазон цен в формате [минимальная цена, максимальная цена]. Позволяет пользователю указать, какие товары его интересуют в пределах заданного ценового диапазона." + }, + "category": { + "type": "string", + "description": "Категория товаров, по которой будет осуществляться фильтрация. Например, 'электроника', 'одежда', 'книги', 'дом и сад'. Это позволяет пользователю сузить поиск до определенной группы товаров." + }, + "sortBy": { + "type": "string", + "enum": ["price", "popularity", "rating"], + "description": "Критерий сортировки товаров. Возможные значения: 'price' для сортировки по цене, 'popularity' для сортировки по популярности, 'rating' для сортировки по рейтингу. Это позволяет пользователю получить список товаров в удобном для него порядке." + } + }, + "required": ["priceMinMax", "category"], + "additionalProperties": False + } + } + } + + balance_tool = { + "function": { + "name": "BalanceTool", + "description": "Получает баланс пользователя из базы данных.", + "parameters": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "Идентификатор пользователя, для которого нужно получить баланс." + } + }, + "required": ["userId"], + "additionalProperties": False + } + } + } + + demo_balance_tool = { + "function": { + "name": "DemoBalanceTool", + "description": "Демонстрационный инструмент, который получает баланс пользователя.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": False + } + } + } + + order_tool = { + "function": { + "name": "OrderTool", + "description": "Демонстрационный инструмент, который заказывает товар по его идентификатору.", + "parameters": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "description": "Идентификатор товара, который нужно заказать." + }, + "quantity": { + "type": "integer", + "description": "Количество товара для заказа.", + "minimum": 1 + } + }, + "required": ["productId", "quantity"], + "additionalProperties": False + } + } + } + + sql_tool = { + "function": { + "name": "ask_database", + "description": f"Use this function to answer user questions about the database. Input should be a fully formed SQL query.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": f""" + SQL query extracting info to answer the user's question. + SQL should be written using this database schema: + {products_table_description} + The query should be returned in plain text, not in JSON. + """, + } + }, + "required": ["query"], + }, + } + } + + tools = [ + weather_tool, + #product_list_tool, + balance_tool, + demo_balance_tool, + order_tool, + sql_tool + ] + + result = chat_completion_request(test_messages, tools=tools) + print(f"result: {result}") + print(f"function call: {get_function_call_from_response(result)}") + toolResults: list = process_functions(get_function_call_from_response(result)) + print(f"tool results: {toolResults}") + + toolCalls: list = get_function_call_from_response(result) + test_messages.append({ + 'role': 'assistant', + #'text': '', + 'toolCallList': {'toolCalls': toolCalls} + }) + + test_messages.append({ + 'role': 'assistant', + #'text': '', + 'toolResultList': {'toolResults': toolResults} + }) + + print(f"test messages: {test_messages}") + + result_final = chat_completion_request(test_messages, tools=tools) + print(f"result final: {result_final}") + with open('output.md', 'w', encoding='utf-8') as f: + assistant_message = result_final['result']['alternatives'][0]['message']['text'] + f.write(assistant_message) + + test_messages.append({ + 'role': 'assistant', + 'text': assistant_message + }) + + print(f"test messages: {test_messages}") + + pretty_print_conversation(test_messages) diff --git a/func_calling.py b/func_calling.py new file mode 100644 index 0000000..dcceea9 --- /dev/null +++ b/func_calling.py @@ -0,0 +1,25 @@ +import os +import json +from openai import OpenAI +from tenacity import retry, wait_random_exponential, stop_after_attempt +from termcolor import colored + +YANDEX_API_KEY = os.getenv('YANDEX_API_KEY') or '' +FOLDER_ID = os.getenv('FOLDER_ID') or '' +GPT_MODEL = 'yandexgpt/latest' + +@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3)) +def chat_completion_request(messages, tools=None, tool_choice=None, model=GPT_MODEL): + try: + response = client.chat.completions.create( + model=model, + messages=messages, + tools=tools, + tool_choice=tool_choice, + ) + return response + except Exception as e: + print("Unable to generate ChatCompletion response") + print(f"Exception: {e}") + return e + diff --git a/index_id.json b/index_id.json new file mode 100644 index 0000000..05b69e2 --- /dev/null +++ b/index_id.json @@ -0,0 +1 @@ +{"index_id": "fvtrbik8thr6b1fh2dhq"} \ No newline at end of file diff --git a/knowledge/data_0/Platon_Gosudarstvo_7_kniga.docx b/knowledge/data_0/Platon_Gosudarstvo_7_kniga.docx new file mode 100644 index 0000000..7f913e2 Binary files /dev/null and b/knowledge/data_0/Platon_Gosudarstvo_7_kniga.docx differ diff --git a/output.md b/output.md new file mode 100644 index 0000000..9441d56 --- /dev/null +++ b/output.md @@ -0,0 +1,16 @@ +Вот список всех товаров категории 'электроника', отсортированных по рейтингу, ценой меньше 30 тысяч: + +1. Смартфон XYZ + - Цена: 19999.99 + - Рейтинг: 4.5 + - Количество заказов: 150 + +2. Планшет JKL + - Цена: 24999.99 + - Рейтинг: 4.3 + - Количество заказов: 100 + +3. Наушники DEF + - Цена: 2999.99 + - Рейтинг: 4.2 + - Количество заказов: 200 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2fe95ad --- /dev/null +++ b/requirements.txt @@ -0,0 +1,33 @@ +annotated-types==0.7.0 +anyio==4.6.2.post1 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.4.0 +cryptography==42.0.8 +distro==1.9.0 +get-annotations==0.1.2 +googleapis-common-protos==1.66.0 +grpcio==1.68.0 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.27.2 +idna==3.10 +jiter==0.7.1 +numpy==2.1.3 +openai==1.55.1 +protobuf==4.25.5 +pycparser==2.22 +pydantic==2.10.1 +pydantic_core==2.27.1 +PyJWT==2.10.0 +requests==2.32.3 +scipy==1.14.1 +six==1.16.0 +sniffio==1.3.1 +tenacity==9.0.0 +termcolor==2.5.0 +tqdm==4.67.1 +typing_extensions==4.12.2 +urllib3==2.2.3 +yandex-cloud-ml-sdk==0.2.0 +yandexcloud==0.325.0