diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ + diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ + diff --git a/app/core/ollama_client.py b/app/core/ollama_client.py new file mode 100644 index 0000000..24465e0 --- /dev/null +++ b/app/core/ollama_client.py @@ -0,0 +1,32 @@ +import json +import urllib.request +import urllib.error + +OLLAMA_URL = "http://192.168.11.3:11434/api/chat" +MODEL_NAME = "qwen2.5:14b" + +def send_ollama_request(messages: list, tools: list) -> dict: + """Отправляет HTTP-запрос к локальной модели Ollama.""" + payload = { + "model": MODEL_NAME, + "messages": messages, + "tools": tools, + "stream": False, + "options": { + "num_predict": 2048, + "num_ctx": 8192, + "temperature": 0.1 + } + } + + req = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.URLError as e: + raise ConnectionError(f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}") diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1 @@ + diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 0000000..0b349de --- /dev/null +++ b/app/db/database.py @@ -0,0 +1,152 @@ +import sqlite3 + +DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" + +def get_connection(): + return sqlite3.connect(DB_NAME) + +def db_get_active_system_prompt() -> str: + """Извлекает активный системный промпт из базы данных SQLite.""" + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute("SELECT prompt_text FROM system_prompts WHERE name = 'main_agent' AND is_active = 1 LIMIT 1") + row = cursor.fetchone() + conn.close() + if row and row[0]: + return row[0].replace("**", "") + except Exception as e: + print(f"Ошибка чтения системного промпта из БД: {e}") + + return "Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI." + +def db_add_system_prompt(name: str = "main_agent", prompt_text: str = "") -> dict: + """Обновляет или добавляет активный системный промпт в БД.""" + clean_text = prompt_text.replace("**", "") + conn = get_connection() + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO system_prompts (name, prompt_text, is_active) + VALUES (?, ?, 1) + ON CONFLICT(name) DO UPDATE SET prompt_text=excluded.prompt_text, updated_at=CURRENT_TIMESTAMP + """, (name, clean_text)) + conn.commit() + conn.close() + return {"status": "success", "message": f"Системный промпт '{name}' успешно обновлен в базе!"} + +def db_get_rules() -> list: + """Извлекает правила из таблицы ai_knowledge_base.""" + conn = get_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return rows + +def db_add_rule(rule_text: str, added_by: str = "Operator") -> dict: + """Добавляет новое правило или инструкцию в таблицу ai_knowledge_base.""" + clean_text = rule_text.replace("**", "") + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "INSERT INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)", + (clean_text, added_by) + ) + rule_id = cursor.lastrowid + conn.commit() + conn.close() + return {"status": "success", "message": f"Правило #{rule_id} успешно добавлено в базу знаний!", "rule_id": rule_id} + +def db_get_tasks(status: str = None) -> list: + """Извлекает список задач из реестра БД.""" + conn = get_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + if status: + cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),)) + else: + cursor.execute("SELECT * FROM tasks ORDER BY task_id") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return rows + +def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: + """Обновляет статус и/или срок выполнения задачи.""" + conn = get_connection() + cursor = conn.cursor() + updates = [] + params = [] + + if status: + status_upper = status.upper() + updates.append("status = ?") + params.append(status_upper) + if status_upper == "COMPLETED": + updates.append("completed_at = CURRENT_TIMESTAMP") + else: + updates.append("completed_at = NULL") + + if due_date: + updates.append("due_date = ?") + params.append(due_date) + + if not updates: + conn.close() + return {"error": "Не указаны параметры для обновления"} + + params.append(task_id.upper()) + query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" + cursor.execute(query, tuple(params)) + + if cursor.rowcount == 0: + conn.close() + return {"error": f"Задача {task_id} не найдена"} + + conn.commit() + conn.close() + return {"status": "success", "message": f"Задача {task_id} обновлена"} + +def db_delete_task(task_id: str) -> dict: + """Удаляет задачу из базы данных по её task_id.""" + conn = get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM tasks WHERE task_id = ?", (task_id.upper(),)) + deleted_count = cursor.rowcount + conn.commit() + conn.close() + + if deleted_count == 0: + return {"error": f"Задача {task_id} не найдена"} + return {"status": "success", "message": f"Задача {task_id} удалена"} + +def db_add_task(module: str = "general", title: str = "", priority: str = "MEDIUM", due_date: str = None, task_id: str = None) -> dict: + """Добавляет новую задачу в реестр с авто-генерацией TASK-ID.""" + conn = get_connection() + cursor = conn.cursor() + + if not task_id: + cursor.execute("SELECT task_id FROM tasks WHERE task_id LIKE 'TASK-%'") + rows = cursor.fetchall() + max_num = 0 + for r in rows: + try: + num = int(r[0].replace("TASK-", "")) + if num > max_num: + max_num = num + except ValueError: + pass + task_id = f"TASK-{max_num + 1:02d}" + + try: + cursor.execute( + "INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)", + (task_id.upper(), module, title, priority.upper(), due_date) + ) + conn.commit() + res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} + except sqlite3.IntegrityError: + res = {"error": f"Задача с ID {task_id} уже существует"} + finally: + conn.close() + return res diff --git a/app/orchestrator.py b/app/orchestrator.py new file mode 100644 index 0000000..dcf4396 --- /dev/null +++ b/app/orchestrator.py @@ -0,0 +1,101 @@ +import json +from typing import List, Dict, Any, Tuple +from datetime import datetime + +from app.db import database +from app.tools.definitions import TOOLS_SCHEMA +from app.tools.handlers import handle_tool_call, get_system_capabilities_text +from app.core.ollama_client import send_ollama_request + +def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: + if chat_history is None: + chat_history = [] + + current_now = datetime.now().strftime("%Y-%m-%d %H:%M") + user_clean = user_message.replace("\xa0", " ").strip() + user_lower = user_clean.lower() + + # 1. Жесткий перехват: Помощь и варианты команд (чистый Plain Text без Markdown) + if any(phrase in user_lower for phrase in ["помощь", "возможности", "что ты умеешь", "помощь в общении", "варианты команд"]): + formatted_text = get_system_capabilities_text() + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 2. Жесткий перехват: Системный промпт из БД + if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы", "промпт из базы"]): + prompt_text = database.db_get_active_system_prompt() + formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 3. Жесткий перехват: База знаний из БД + if any(phrase in user_lower for phrase in ["базу знаний", "база знаний", "покажи правила", "инструкции ии"]): + rules = database.db_get_rules() + if not rules: + formatted_text = "База знаний пуста." + else: + formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" + for r in rules: + formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # Динамическая подгрузка системного промпта из БД SQLite + dynamic_prompt_text = database.db_get_active_system_prompt() + system_prompt = { + "role": "system", + "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" + } + + messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] + + try: + res_data = send_ollama_request(messages=messages, tools=TOOLS_SCHEMA) + msg = res_data.get("message", {}) + + tool_calls = msg.get("tool_calls", []) + content_str = msg.get("content", "").strip().replace("**", "").replace("```", "").replace("###", "") + + # Перехват текстового JSON с именем функции + if not tool_calls and "name" in content_str and "db_" in content_str: + try: + start_idx = content_str.find("{") + end_idx = content_str.rfind("}") + 1 + if start_idx != -1 and end_idx != -1: + parsed = json.loads(content_str[start_idx:end_idx]) + if "name" in parsed: + tool_calls = [{"function": parsed}] + except Exception: + pass + + if tool_calls: + for tool in tool_calls: + fn_name = tool["function"]["name"] + fn_args = tool["function"].get("arguments", {}) + + formatted_text = handle_tool_call(fn_name, fn_args) + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": content_str} + ] + return content_str, updated_history + + except Exception as e: + return f"Ошибка обработки запроса: {e}", chat_history \ No newline at end of file diff --git a/app/tools/__init__.py b/app/tools/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/tools/__init__.py @@ -0,0 +1 @@ + diff --git a/app/tools/definitions.py b/app/tools/definitions.py new file mode 100644 index 0000000..34f4a8d --- /dev/null +++ b/app/tools/definitions.py @@ -0,0 +1,111 @@ +TOOLS_SCHEMA = [ + { + "type": "function", + "function": { + "name": "db_get_tasks", + "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." + } + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_rules", + "description": "ПОЛУЧИТЬ БАЗУ ЗНАНИЙ ИИ И ПРАВИЛА АРБИТРАЖА (ai_knowledge_base). Вызывай когда пользователь просит показать базу знаний, правила, инструкции или промпты.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_add_rule", + "description": "Добавить новое правило, инструкцию или факт в базу знаний (ai_knowledge_base). Вызывай при командах 'добавь в базу знаний', 'запомни правило', 'запиши инструкцию'.", + "parameters": { + "type": "object", + "properties": { + "rule_text": {"type": "string", "description": "Текст правила, инструкции или факта для сохранения"}, + "added_by": {"type": "string", "description": "Автор или источник правила, по умолчанию Operator"} + }, + "required": ["rule_text"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_system_prompt", + "description": "ПОЛУЧИТЬ ТЕКУЩИЙ СИСТЕМНЫЙ ПРОМПТ ИИ (system_prompts). Вызывай когда пользователь просит показать системный промпт, инструкции ассистента или промпт из базы.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_add_system_prompt", + "description": "Обновить или добавить системный промпт ИИ в базу данных. Вызывай при командах 'задай системный промпт', 'измени промпт', 'обнови системный промпт'.", + "parameters": { + "type": "object", + "properties": { + "prompt_text": {"type": "string", "description": "Полный новый текст системного промпта"}, + "name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"} + }, + "required": ["prompt_text"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_update_task_status", + "description": "Изменить статус и/или срок выполнения задачи в реестре.", + "parameters": { + "type": "object", + "properties": { + "task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"}, + "status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_delete_task", + "description": "Удалить задачу из реестра по её task_id (например, TASK-18).", + "parameters": { + "type": "object", + "properties": { + "task_id": {"type": "string", "description": "Идентификатор задачи для удаления, например TASK-18"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_add_task", + "description": "Добавить новую задачу в бэклог проекта.", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Краткое описание задачи"}, + "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, + "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} + }, + "required": ["title"] + } + } + } +] diff --git a/app/tools/handlers.py b/app/tools/handlers.py new file mode 100644 index 0000000..259043a --- /dev/null +++ b/app/tools/handlers.py @@ -0,0 +1,101 @@ +from app.db import database + +def get_system_capabilities_text() -> str: + """Формирует список возможностей и примеров команд для текущей стадии проекта.""" + return ( + "🛠 Доступные команды и возможности системы SCUD Orion AI:\n\n" + "1. Управление задачами (Бэклог):\n" + " • «Покажи последние задачи» — вывод списка задач проекта.\n" + " • «Добавь задачу: Настроить интеграцию 1С с REST API» — создание новой задачи.\n" + " • «Измени статус задачи TASK-01 на COMPLETED» — обновление состояния.\n" + " • «Удали задачу TASK-02» — удаление задачи из реестра.\n\n" + "2. База знаний и правила (ai_knowledge_base):\n" + " • «Покажи базу знаний» (или «покажи правила») — вывод текущих правил проекта.\n" + " • «Добавь в базу знаний: Офисные сотрудники отдела разработки работают по гибкому графику с 09:00 до 11:00» — запись нового правила.\n\n" + "3. Системные промпты (system_prompts):\n" + " • «Покажи системный промпт» — просмотр активных инструкций ИИ.\n" + " • «Обнови системный промпт: Ты — ассистент SCUD Orion AI. Отвечай кратко и строго в формате Plain Text.» — смена роли ИИ на лету.\n\n" + "Вы можете написать любой из этих запросов в чат в свободной форме." + ) + +def handle_tool_call(fn_name: str, fn_args: dict) -> str: + """Диспетчеризация вызова функций и форматирование текстового ответа.""" + + if fn_name == "db_get_tasks": + tasks = database.db_get_tasks(status=fn_args.get("status")) + if not tasks: + return "Список задач пуст." + tasks_sorted = sorted(tasks, key=lambda x: x.get('task_id', ''), reverse=True) + formatted_text = "📋 Последние задачи проекта:\n\n" + for t in tasks_sorted: + due = f" (до {t['due_date']})" if t.get('due_date') else "" + priority = t.get('priority') or 'MEDIUM' + status_str = t.get('status') or 'BACKLOG' + module_str = t.get('module') or 'general' + formatted_text += f"{t.get('task_id')} — {t.get('title')} [{status_str}]\n" + formatted_text += f" • Приоритет: {priority} | Модуль: {module_str}{due}\n\n" + return formatted_text + + elif fn_name == "db_get_rules": + rules = database.db_get_rules() + if not rules: + return "База знаний пуста." + formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" + for r in rules: + formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" + return formatted_text + + elif fn_name == "db_add_rule": + res = database.db_add_rule( + rule_text=fn_args.get("rule_text", ""), + added_by=fn_args.get("added_by", "Operator") + ) + return res.get("message", "Правило добавлено в базу знаний.") + + elif fn_name == "db_get_system_prompt": + prompt_text = database.db_get_active_system_prompt() + return f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" + + elif fn_name == "db_add_system_prompt": + res = database.db_add_system_prompt( + name=fn_args.get("name", "main_agent"), + prompt_text=fn_args.get("prompt_text", "") + ) + return res.get("message", "Промпт обновлен.") + + elif fn_name == "db_add_task": + res = database.db_add_task( + module=fn_args.get("module", "general"), + title=fn_args.get("title"), + priority=fn_args.get("priority", "MEDIUM"), + due_date=fn_args.get("due_date") + ) + if "error" in res: + return f"Ошибка добавления задачи: {res['error']}" + assigned_id = res.get("task_id", "TASK") + due_str = fn_args.get('due_date') or 'Без срока' + return ( + f"Задача {assigned_id} успешно создана!\n\n" + f"• Название: {fn_args.get('title')}\n" + f"• Приоритет: {fn_args.get('priority', 'MEDIUM')}\n" + f"• Срок: {due_str}\n" + f"• Модуль: {fn_args.get('module', 'general')}" + ) + + elif fn_name == "db_update_task_status": + res = database.db_update_task_status( + task_id=fn_args.get("task_id"), + status=fn_args.get("status"), + due_date=fn_args.get("due_date") + ) + if "error" in res: + return f"Ошибка обновления задачи: {res['error']}" + return f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" + + elif fn_name == "db_delete_task": + res = database.db_delete_task(task_id=fn_args.get("task_id")) + if "error" in res: + return f"Ошибка удаления задачи: {res['error']}" + return f"Задача {fn_args.get('task_id')} успешно удалена из базы!" + + return f"Функция {fn_name} выполнена." \ No newline at end of file diff --git a/context_memory.db b/context_memory.db new file mode 100644 index 0000000..0f2bc8f Binary files /dev/null and b/context_memory.db differ diff --git a/data/scud_orion_ai.db b/data/scud_orion_ai.db index 1761825..90f2085 100644 Binary files a/data/scud_orion_ai.db and b/data/scud_orion_ai.db differ diff --git a/data/static_reason_workers.csv b/data/static_reason_workers.csv index 57e5807..49f9ef7 100644 --- a/data/static_reason_workers.csv +++ b/data/static_reason_workers.csv @@ -9,4 +9,6 @@ fio,reason,note Пшеничный Виктор Петрович,Удаленная работа,Постоянная удаленка Незнанова Валерия Игоревна,Удаленная работа,Постоянная удаленка Ковалев Владимир Владимирович,Удаленная работа,Постоянная удаленка -Кожокарь Татьяна Юрьевна,Удаленная работа,Постоянная удаленка \ No newline at end of file +Кожокарь Татьяна Юрьевна,Удаленная работа,Постоянная удаленка +Субетто Юлия Викторовна,Удаленная работа,Постоянная удаленка +Ароян Станислав Месропович,Удаленная работа,Временная удаленка \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c7508ba --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,342 @@ +cat << 'EOF' > /home/puh/scud_orion_ai_v2/frontend/index.html + + + + + + SCUD Orion AI — Context Manager + + + + + + + +
+
+
+ +
+
+

SCUD Orion AI

+ Context & Task Manager +
+
+ +
+ + API Online + + +
+
+ + +
+
+ +
+

+ ИИ-Ассистент +

+

+ Для вывода возможных команд управления локальными системами, спросите об этом в произвольном формате или нажмите на кнопку ниже. +

+ +
+
+ + +
+
+
+ +
+ +
+ + +
+ +
+
+
+ + + + + + + + + +EOF + +# Копируем файл в корень для совместимости с веб-сервером +cp /home/puh/scud_orion_ai_v2/frontend/index.html /home/puh/scud_orion_ai_v2/index.html \ No newline at end of file diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..e279610 --- /dev/null +++ b/init_db.py @@ -0,0 +1,78 @@ +import sqlite3 +import os +from datetime import datetime + +DB_NAME = "context_memory.db" + +def init_db(): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + # 1. Таблица трекинга задач + cursor.execute(""" + CREATE TABLE IF NOT EXISTS tasks ( + task_id TEXT PRIMARY KEY, + module TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL, + priority TEXT NOT NULL, + completed_at TEXT + ) + """) + + # 2. Таблица архитектурных правил и контекста + cursor.execute(""" + CREATE TABLE IF NOT EXISTS architecture_memory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category TEXT NOT NULL, + rule_text TEXT NOT NULL, + is_active INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Первичное заполнение реестра задач [TASK-01] .. [TASK-17] + tasks_data = [ + ("TASK-01", "services/data_loader", "Исправление KeyError по ФИО в Excel экспорте 1С (переход на .iloc)", "COMPLETED", "HIGH", "2026-08-03"), + ("TASK-02", "services/zup_extractor", "Перевод подключения 1С:ЗУП на продакшн MS SQL сервер ACCOUNT-01 + гибридный фоллбэк", "COMPLETED", "HIGH", "2026-08-04"), + ("TASK-03", "services/scud_export", "Инцидент Равина В.Э.: Фиксация 100% приоритета сырых логов pLogData СКУД над расчетом УРВ Ориона", "COMPLETED", "HIGH", "2026-08-04"), + ("TASK-04", "core/database", "Реализация схемы SQLite WAL, сохранение снапшотов и маппинг колонок (department/position)", "COMPLETED", "HIGH", "2026-08-05"), + ("TASK-05", "core/database", "Исправление бага дублирования номеров снапшотов (переход на вычисление MAX() по суффиксу)", "COMPLETED", "HIGH", "2026-08-05"), + ("TASK-06", "scripts/fix_snapshots", "Создание и выполнение скрипта сквозной переиндексации снапшотов с префиксом Y", "COMPLETED", "HIGH", "2026-08-05"), + ("TASK-07", "scripts/db_cli", "Реализация CLI-инспектора БД и выравнивание номеров срезов для идеальной верстки в терминале", "COMPLETED", "HIGH", "2026-08-05"), + ("TASK-08", "main.py / logic", "Двойная проверка исключений по department_scud и 1С, удержание 100% приоритета кадровых документов", "COMPLETED", "HIGH", "2026-08-05"), + ("TASK-09", "services/text_reporter", "Лаконичный вывод неразмеченных сотрудников с короткими аббревиатурами отделов СКУД (РУК, ОИЗ, ПУ и др.)", "COMPLETED", "HIGH", "2026-08-05"), + ("TASK-10", "services/scud_export", "Внутридневной контроль: Извлечение полных цепочек проходов pLogData (входы/выходы за смену)", "BACKLOG", "HIGH", None), + ("TASK-11", "services/data_loader", "Внутридневной контроль: Расчет количества и суммарной длительности отлучек/перекуров сверх обеда", "BACKLOG", "HIGH", None), + ("TASK-12", "services/text_reporter", "Внутридневной контроль: Вывод списка системных нарушителей внутреннего распорядка в Markdown-отчет", "BACKLOG", "MEDIUM", None), + ("TASK-13", "automation", "Автоматизация сбора срезов по расписанию (Cron / Systemd Timers на 12:00, 17:00, 19:00)", "BACKLOG", "HIGH", None), + ("TASK-14", "services/notifications", "Интеграция Telegram-бота для алертов Администратору при массовых сбоях турникетов (>5%)", "BACKLOG", "MEDIUM", None), + ("TASK-15", "api / fastapi", "Создание REST API на FastAPI (/api/v1/snapshots, /api/v1/scud/logs, /api/v1/anomalies)", "BACKLOG", "HIGH", None), + ("TASK-16", "frontend / chat", "Веб-интерфейс с ИИ-чатом на базе Ollama (Qwen 2.5 14B) с поддержкой Function Calling к REST API", "BACKLOG", "HIGH", None), + ("TASK-17", "core/memory", "Создание модуля персистентной памяти проекта в SQLite и CLI-инструмента для фиксации архитектурных решений", "IN_PROGRESS", "HIGH", None), + ] + + cursor.executemany(""" + INSERT OR REPLACE INTO tasks (task_id, module, title, status, priority, completed_at) + VALUES (?, ?, ?, ?, ?, ?) + """, tasks_data) + + # Базовые правила архитектуры + rules_data = [ + ("logic_rule", "Сырые логи pLogData СКУД имееют 100% приоритет над расчетом УРВ Ориона."), + ("logic_rule", "Официальные кадровые документы 1С (отпуска, командировки, больничные) имеют абсолютный приоритет над отсутствием в СКУД."), + ("scud_override", "Запрещено переименовывать ФИО сотрудников при 100% совпадении строк между 1С и СКУД в AI-верификаторе."), + ("formatting", "Температура указывается исключительно в градусах Цельсия, скорости в км/ч.") + ] + + cursor.executemany(""" + INSERT INTO architecture_memory (category, rule_text) + VALUES (?, ?) + """, rules_data) + + conn.commit() + conn.close() + print("[✓] База данных контекста и трекинга задач успешно создана и заполнена!") + +if __name__ == "__main__": + init_db() \ No newline at end of file diff --git a/llm_agent.py b/llm_agent.py new file mode 100644 index 0000000..867274b --- /dev/null +++ b/llm_agent.py @@ -0,0 +1,474 @@ +import sqlite3 +import json +import urllib.request +import urllib.error +from typing import List, Dict, Any, Tuple +from datetime import datetime + +# Абсолютный путь к базе данных проекта scud_orion_ai_v2 +DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" +OLLAMA_URL = "http://192.168.11.3:11434/api/chat" +MODEL_NAME = "qwen2.5:14b" + + +# === БЛОК РАБОТЫ С БАЗОЙ ДАННЫХ === + +def db_get_active_system_prompt() -> str: + """Извлекает активный системный промпт из базы данных SQLite.""" + try: + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("SELECT prompt_text FROM system_prompts WHERE name = 'main_agent' AND is_active = 1 LIMIT 1") + row = cursor.fetchone() + conn.close() + if row and row[0]: + # Чистим от случайных звездочек + return row[0].replace("**", "") + except Exception as e: + print(f"Ошибка чтения системного промпта из БД: {e}") + + return "Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI." + + +def db_add_system_prompt(name: str = "main_agent", prompt_text: str = "") -> dict: + """Обновляет или добавляет активный системный промпт в БД.""" + clean_text = prompt_text.replace("**", "") + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO system_prompts (name, prompt_text, is_active) + VALUES (?, ?, 1) + ON CONFLICT(name) DO UPDATE SET prompt_text=excluded.prompt_text, updated_at=CURRENT_TIMESTAMP + """, (name, clean_text)) + conn.commit() + conn.close() + return {"status": "success", "message": f"Системный промпт '{name}' успешно обновлен в базе!"} + + +def db_get_tasks(status: str = None) -> list: + """Извлекает список задач из реестра БД.""" + conn = sqlite3.connect(DB_NAME) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + if status: + cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),)) + else: + cursor.execute("SELECT * FROM tasks ORDER BY task_id") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return rows + + +def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: + """Обновляет статус и/или срок выполнения задачи.""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + updates = [] + params = [] + + if status: + status_upper = status.upper() + updates.append("status = ?") + params.append(status_upper) + if status_upper == "COMPLETED": + updates.append("completed_at = CURRENT_TIMESTAMP") + else: + updates.append("completed_at = NULL") + + if due_date: + updates.append("due_date = ?") + params.append(due_date) + + if not updates: + conn.close() + return {"error": "Не указаны параметры для обновления"} + + params.append(task_id.upper()) + query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" + cursor.execute(query, tuple(params)) + + if cursor.rowcount == 0: + conn.close() + return {"error": f"Задача {task_id} не найдена"} + + conn.commit() + conn.close() + return {"status": "success", "message": f"Задача {task_id} обновлена"} + + +def db_delete_task(task_id: str) -> dict: + """Удаляет задачу из базы данных по её task_id.""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("DELETE FROM tasks WHERE task_id = ?", (task_id.upper(),)) + deleted_count = cursor.rowcount + conn.commit() + conn.close() + + if deleted_count == 0: + return {"error": f"Задача {task_id} не найдена"} + return {"status": "success", "message": f"Задача {task_id} удалена"} + + +def db_add_task(module: str = "general", title: str = "", priority: str = "MEDIUM", due_date: str = None, task_id: str = None) -> dict: + """Добавляет новую задачу в реестр с авто-генерацией TASK-ID.""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + if not task_id: + cursor.execute("SELECT task_id FROM tasks WHERE task_id LIKE 'TASK-%'") + rows = cursor.fetchall() + max_num = 0 + for r in rows: + try: + num = int(r[0].replace("TASK-", "")) + if num > max_num: + max_num = num + except ValueError: + pass + task_id = f"TASK-{max_num + 1:02d}" + + try: + cursor.execute( + "INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)", + (task_id.upper(), module, title, priority.upper(), due_date) + ) + conn.commit() + res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} + except sqlite3.IntegrityError: + res = {"error": f"Задача с ID {task_id} уже существует"} + finally: + conn.close() + return res + + +def db_get_rules() -> list: + """Извлекает правила из таблицы ai_knowledge_base.""" + conn = sqlite3.connect(DB_NAME) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return rows + + +# === СХЕМА ИНСТРУМЕНТОВ ДЛЯ OLLAMA === + +TOOLS_SCHEMA = [ + { + "type": "function", + "function": { + "name": "db_get_tasks", + "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." + } + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_rules", + "description": "ПОЛУЧИТЬ БАЗУ ЗНАНИЙ ИИ И ПРАВИЛА АРБИТРАЖА (ai_knowledge_base). Вызывай когда пользователь просит показать базу знаний, правила, инструкции или промпты.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_get_system_prompt", + "description": "ПОЛУЧИТЬ ТЕКУЩИЙ СИСТЕМНЫЙ ПРОМПТ ИИ (system_prompts). Вызывай когда пользователь просит показать системный промпт, инструкции ассистента или промпт из базы.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_add_system_prompt", + "description": "Обновить или добавить системный промпт ИИ в базу данных. Вызывай при командах 'задай системный промпт', 'измени промпт', 'обнови системный промпт'.", + "parameters": { + "type": "object", + "properties": { + "prompt_text": {"type": "string", "description": "Полный новый текст системного промпта"}, + "name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"} + }, + "required": ["prompt_text"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_update_task_status", + "description": "Изменить статус и/или срок выполнения задачи в реестре.", + "parameters": { + "type": "object", + "properties": { + "task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"}, + "status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_delete_task", + "description": "Удалить задачу из реестра по её task_id (например, TASK-18).", + "parameters": { + "type": "object", + "properties": { + "task_id": {"type": "string", "description": "Идентификатор задачи для удаления, например TASK-18"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_add_task", + "description": "Добавить новую задачу в бэклог проекта.", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Краткое описание задачи"}, + "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, + "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} + }, + "required": ["title"] + } + } + } +] + + +# === ОСНОВНАЯ ФУНКЦИЯ ОБРАБОТКИ СООБЩЕНИЙ === + +def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: + if chat_history is None: + chat_history = [] + + current_now = datetime.now().strftime("%Y-%m-%d %H:%M") + user_lower = user_message.lower().strip() + + # Жесткие перехваты для 100% чистого вывода без участия генератора Ollama (без звездочек!) + if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы", "промпт из базы"]): + prompt_text = db_get_active_system_prompt() + formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + if any(phrase in user_lower for phrase in ["базу знаний", "база знаний", "покажи правила", "инструкции ии"]): + rules = db_get_rules() + if not rules: + formatted_text = "База знаний пуста." + else: + formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" + for r in rules: + formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # Динамическая подгрузка системного промпта из БД SQLite + dynamic_prompt_text = db_get_active_system_prompt() + + system_prompt = { + "role": "system", + "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" + } + + messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] + + payload = { + "model": MODEL_NAME, + "messages": messages, + "tools": TOOLS_SCHEMA, + "stream": False, + "options": { + "num_predict": 2048, + "num_ctx": 8192, + "temperature": 0.1 + } + } + + req = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + try: + with urllib.request.urlopen(req) as response: + res_data = json.loads(response.read().decode("utf-8")) + msg = res_data.get("message", {}) + + tool_calls = msg.get("tool_calls", []) + content_str = msg.get("content", "").strip().replace("**", "") + + # Перехват вызова функции из текста + if not tool_calls and "name" in content_str and "db_" in content_str: + try: + start_idx = content_str.find("{") + end_idx = content_str.rfind("}") + 1 + if start_idx != -1 and end_idx != -1: + parsed = json.loads(content_str[start_idx:end_idx]) + if "name" in parsed: + tool_calls = [{"function": parsed}] + except Exception: + pass + + if tool_calls: + for tool in tool_calls: + fn_name = tool["function"]["name"] + fn_args = tool["function"].get("arguments", {}) + + # 1. Вывод задач + if fn_name == "db_get_tasks": + tasks = db_get_tasks(status=fn_args.get("status")) + if not tasks: + formatted_text = "Список задач пуст." + else: + tasks_sorted = sorted(tasks, key=lambda x: x.get('task_id', ''), reverse=True) + formatted_text = "📋 Последние задачи проекта:\n\n" + for t in tasks_sorted: + due = f" (до {t['due_date']})" if t.get('due_date') else "" + priority = t.get('priority') or 'MEDIUM' + status_str = t.get('status') or 'BACKLOG' + module_str = t.get('module') or 'general' + + formatted_text += f"{t.get('task_id')} — {t.get('title')} [{status_str}]\n" + formatted_text += f" • Приоритет: {priority} | Модуль: {module_str}{due}\n\n" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 2. Вывод базы знаний + elif fn_name == "db_get_rules": + rules = db_get_rules() + if not rules: + formatted_text = "База знаний пуста." + else: + formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" + for r in rules: + formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 3. Вывод системного промпта + elif fn_name == "db_get_system_prompt": + prompt_text = db_get_active_system_prompt() + formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 4. Добавление/обновление системного промпта + elif fn_name == "db_add_system_prompt": + res = db_add_system_prompt( + name=fn_args.get("name", "main_agent"), + prompt_text=fn_args.get("prompt_text", "") + ) + formatted_text = res.get("message", "Промпт обновлен.") + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 5. Добавление новой задачи + elif fn_name == "db_add_task": + res = db_add_task( + module=fn_args.get("module", "general"), + title=fn_args.get("title"), + priority=fn_args.get("priority", "MEDIUM"), + due_date=fn_args.get("due_date") + ) + + if "error" in res: + formatted_text = f"Ошибка добавления задачи: {res['error']}" + else: + assigned_id = res.get("task_id", "TASK") + due_str = fn_args.get('due_date') or 'Без срока' + formatted_text = ( + f"Задача {assigned_id} успешно создана!\n\n" + f"• Название: {fn_args.get('title')}\n" + f"• Приоритет: {fn_args.get('priority', 'MEDIUM')}\n" + f"• Срок: {due_str}\n" + f"• Модуль: {fn_args.get('module', 'general')}" + ) + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 6. Обновление статуса задачи + elif fn_name == "db_update_task_status": + res = db_update_task_status( + task_id=fn_args.get("task_id"), + status=fn_args.get("status"), + due_date=fn_args.get("due_date") + ) + + if "error" in res: + formatted_text = f"Ошибка обновления задачи: {res['error']}" + else: + formatted_text = f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # 7. Удаление задачи + elif fn_name == "db_delete_task": + res = db_delete_task(task_id=fn_args.get("task_id")) + if "error" in res: + formatted_text = f"Ошибка удаления задачи: {res['error']}" + else: + formatted_text = f"Задача {fn_args.get('task_id')} успешно удалена из базы!" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": content_str} + ] + return content_str, updated_history + + except urllib.error.URLError as e: + return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history \ No newline at end of file diff --git a/ref.py b/ref.py new file mode 100644 index 0000000..7359194 --- /dev/null +++ b/ref.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python3 +import os + +# Базовый путь к проекту +BASE_DIR = "/home/puh/scud_orion_ai_v2" + +# 1. Данные для файла app/db/database.py +DB_PY = '''import sqlite3 + +DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" + +def get_connection(): + return sqlite3.connect(DB_NAME) + +def db_get_active_system_prompt() -> str: + """Извлекает активный системный промпт из базы данных SQLite.""" + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute("SELECT prompt_text FROM system_prompts WHERE name = 'main_agent' AND is_active = 1 LIMIT 1") + row = cursor.fetchone() + conn.close() + if row and row[0]: + return row[0].replace("**", "") + except Exception as e: + print(f"Ошибка чтения системного промпта из БД: {e}") + + return "Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI." + +def db_add_system_prompt(name: str = "main_agent", prompt_text: str = "") -> dict: + """Обновляет или добавляет активный системный промпт в БД.""" + clean_text = prompt_text.replace("**", "") + conn = get_connection() + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO system_prompts (name, prompt_text, is_active) + VALUES (?, ?, 1) + ON CONFLICT(name) DO UPDATE SET prompt_text=excluded.prompt_text, updated_at=CURRENT_TIMESTAMP + """, (name, clean_text)) + conn.commit() + conn.close() + return {"status": "success", "message": f"Системный промпт '{name}' успешно обновлен в базе!"} + +def db_get_rules() -> list: + """Извлекает правила из таблицы ai_knowledge_base.""" + conn = get_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return rows + +def db_add_rule(rule_text: str, added_by: str = "Operator") -> dict: + """Добавляет новое правило или инструкцию в таблицу ai_knowledge_base.""" + clean_text = rule_text.replace("**", "") + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "INSERT INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)", + (clean_text, added_by) + ) + rule_id = cursor.lastrowid + conn.commit() + conn.close() + return {"status": "success", "message": f"Правило #{rule_id} успешно добавлено в базу знаний!", "rule_id": rule_id} + +def db_get_tasks(status: str = None) -> list: + """Извлекает список задач из реестра БД.""" + conn = get_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + if status: + cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),)) + else: + cursor.execute("SELECT * FROM tasks ORDER BY task_id") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return rows + +def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: + """Обновляет статус и/или срок выполнения задачи.""" + conn = get_connection() + cursor = conn.cursor() + updates = [] + params = [] + + if status: + status_upper = status.upper() + updates.append("status = ?") + params.append(status_upper) + if status_upper == "COMPLETED": + updates.append("completed_at = CURRENT_TIMESTAMP") + else: + updates.append("completed_at = NULL") + + if due_date: + updates.append("due_date = ?") + params.append(due_date) + + if not updates: + conn.close() + return {"error": "Не указаны параметры для обновления"} + + params.append(task_id.upper()) + query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" + cursor.execute(query, tuple(params)) + + if cursor.rowcount == 0: + conn.close() + return {"error": f"Задача {task_id} не найдена"} + + conn.commit() + conn.close() + return {"status": "success", "message": f"Задача {task_id} обновлена"} + +def db_delete_task(task_id: str) -> dict: + """Удаляет задачу из базы данных по её task_id.""" + conn = get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM tasks WHERE task_id = ?", (task_id.upper(),)) + deleted_count = cursor.rowcount + conn.commit() + conn.close() + + if deleted_count == 0: + return {"error": f"Задача {task_id} не найдена"} + return {"status": "success", "message": f"Задача {task_id} удалена"} + +def db_add_task(module: str = "general", title: str = "", priority: str = "MEDIUM", due_date: str = None, task_id: str = None) -> dict: + """Добавляет новую задачу в реестр с авто-генерацией TASK-ID.""" + conn = get_connection() + cursor = conn.cursor() + + if not task_id: + cursor.execute("SELECT task_id FROM tasks WHERE task_id LIKE 'TASK-%'") + rows = cursor.fetchall() + max_num = 0 + for r in rows: + try: + num = int(r[0].replace("TASK-", "")) + if num > max_num: + max_num = num + except ValueError: + pass + task_id = f"TASK-{max_num + 1:02d}" + + try: + cursor.execute( + "INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)", + (task_id.upper(), module, title, priority.upper(), due_date) + ) + conn.commit() + res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} + except sqlite3.IntegrityError: + res = {"error": f"Задача с ID {task_id} уже существует"} + finally: + conn.close() + return res +''' + +# 2. Данные для файла app/tools/definitions.py +TOOLS_DEFINITIONS_PY = '''TOOLS_SCHEMA = [ + { + "type": "function", + "function": { + "name": "db_get_tasks", + "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." + } + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_rules", + "description": "ПОЛУЧИТЬ БАЗУ ЗНАНИЙ ИИ И ПРАВИЛА АРБИТРАЖА (ai_knowledge_base). Вызывай когда пользователь просит показать базу знаний, правила, инструкции или промпты.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_add_rule", + "description": "Добавить новое правило, инструкцию или факт в базу знаний (ai_knowledge_base). Вызывай при командах 'добавь в базу знаний', 'запомни правило', 'запиши инструкцию'.", + "parameters": { + "type": "object", + "properties": { + "rule_text": {"type": "string", "description": "Текст правила, инструкции или факта для сохранения"}, + "added_by": {"type": "string", "description": "Автор или источник правила, по умолчанию Operator"} + }, + "required": ["rule_text"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_system_prompt", + "description": "ПОЛУЧИТЬ ТЕКУЩИЙ СИСТЕМНЫЙ ПРОМПТ ИИ (system_prompts). Вызывай когда пользователь просит показать системный промпт, инструкции ассистента или промпт из базы.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_add_system_prompt", + "description": "Обновить или добавить системный промпт ИИ в базу данных. Вызывай при командах 'задай системный промпт', 'измени промпт', 'обнови системный промпт'.", + "parameters": { + "type": "object", + "properties": { + "prompt_text": {"type": "string", "description": "Полный новый текст системного промпта"}, + "name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"} + }, + "required": ["prompt_text"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_update_task_status", + "description": "Изменить статус и/или срок выполнения задачи в реестре.", + "parameters": { + "type": "object", + "properties": { + "task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"}, + "status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_delete_task", + "description": "Удалить задачу из реестра по её task_id (например, TASK-18).", + "parameters": { + "type": "object", + "properties": { + "task_id": {"type": "string", "description": "Идентификатор задачи для удаления, например TASK-18"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_add_task", + "description": "Добавить новую задачу в бэклог проекта.", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Краткое описание задачи"}, + "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, + "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} + }, + "required": ["title"] + } + } + } +] +''' + +# 3. Данные для файла app/tools/handlers.py +TOOLS_HANDLERS_PY = '''from app.db import database + +def handle_tool_call(fn_name: str, fn_args: dict) -> str: + """Диспетчеризация вызова функций и форматирование текстового ответа.""" + + if fn_name == "db_get_tasks": + tasks = database.db_get_tasks(status=fn_args.get("status")) + if not tasks: + return "Список задач пуст." + tasks_sorted = sorted(tasks, key=lambda x: x.get('task_id', ''), reverse=True) + formatted_text = "📋 Последние задачи проекта:\n\n" + for t in tasks_sorted: + due = f" (до {t['due_date']})" if t.get('due_date') else "" + priority = t.get('priority') or 'MEDIUM' + status_str = t.get('status') or 'BACKLOG' + module_str = t.get('module') or 'general' + formatted_text += f"{t.get('task_id')} — {t.get('title')} [{status_str}]\n" + formatted_text += f" • Приоритет: {priority} | Модуль: {module_str}{due}\n\n" + return formatted_text + + elif fn_name == "db_get_rules": + rules = database.db_get_rules() + if not rules: + return "База знаний пуста." + formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" + for r in rules: + formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" + return formatted_text + + elif fn_name == "db_add_rule": + res = database.db_add_rule( + rule_text=fn_args.get("rule_text", ""), + added_by=fn_args.get("added_by", "Operator") + ) + return res.get("message", "Правило добавлено в базу знаний.") + + elif fn_name == "db_get_system_prompt": + prompt_text = database.db_get_active_system_prompt() + return f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" + + elif fn_name == "db_add_system_prompt": + res = database.db_add_system_prompt( + name=fn_args.get("name", "main_agent"), + prompt_text=fn_args.get("prompt_text", "") + ) + return res.get("message", "Промпт обновлен.") + + elif fn_name == "db_add_task": + res = database.db_add_task( + module=fn_args.get("module", "general"), + title=fn_args.get("title"), + priority=fn_args.get("priority", "MEDIUM"), + due_date=fn_args.get("due_date") + ) + if "error" in res: + return f"Ошибка добавления задачи: {res['error']}" + assigned_id = res.get("task_id", "TASK") + due_str = fn_args.get('due_date') or 'Без срока' + return ( + f"Задача {assigned_id} успешно создана!\n\n" + f"• Название: {fn_args.get('title')}\n" + f"• Приоритет: {fn_args.get('priority', 'MEDIUM')}\n" + f"• Срок: {due_str}\n" + f"• Модуль: {fn_args.get('module', 'general')}" + ) + + elif fn_name == "db_update_task_status": + res = database.db_update_task_status( + task_id=fn_args.get("task_id"), + status=fn_args.get("status"), + due_date=fn_args.get("due_date") + ) + if "error" in res: + return f"Ошибка обновления задачи: {res['error']}" + return f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" + + elif fn_name == "db_delete_task": + res = database.db_delete_task(task_id=fn_args.get("task_id")) + if "error" in res: + return f"Ошибка удаления задачи: {res['error']}" + return f"Задача {fn_args.get('task_id')} успешно удалена из базы!" + + return f"Функция {fn_name} выполнена." +''' + +# 4. Данные для файла app/core/ollama_client.py +OLLAMA_CLIENT_PY = '''import json +import urllib.request +import urllib.error + +OLLAMA_URL = "http://192.168.11.3:11434/api/chat" +MODEL_NAME = "qwen2.5:14b" + +def send_ollama_request(messages: list, tools: list) -> dict: + """Отправляет HTTP-запрос к локальной модели Ollama.""" + payload = { + "model": MODEL_NAME, + "messages": messages, + "tools": tools, + "stream": False, + "options": { + "num_predict": 2048, + "num_ctx": 8192, + "temperature": 0.1 + } + } + + req = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.URLError as e: + raise ConnectionError(f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}") +''' + +# 5. Данные для файла app/orchestrator.py +ORCHESTRATOR_PY = '''import json +from typing import List, Dict, Any, Tuple +from datetime import datetime + +from app.db import database +from app.tools.definitions import TOOLS_SCHEMA +from app.tools.handlers import handle_tool_call +from app.core.ollama_client import send_ollama_request + +def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: + if chat_history is None: + chat_history = [] + + current_now = datetime.now().strftime("%Y-%m-%d %H:%M") + user_lower = user_message.lower().strip() + + # Быстрые жесткие перехваты + if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы", "промпт из базы"]): + prompt_text = database.db_get_active_system_prompt() + formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + if any(phrase in user_lower for phrase in ["базу знаний", "база знаний", "покажи правила", "инструкции ии"]): + rules = database.db_get_rules() + if not rules: + formatted_text = "База знаний пуста." + else: + formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" + for r in rules: + formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + # Загрузка актуального промпта + dynamic_prompt_text = database.db_get_active_system_prompt() + system_prompt = { + "role": "system", + "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" + } + + messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] + + try: + res_data = send_ollama_request(messages=messages, tools=TOOLS_SCHEMA) + msg = res_data.get("message", {}) + + tool_calls = msg.get("tool_calls", []) + content_str = msg.get("content", "").strip().replace("**", "") + + # Перехват текстового JSON с именем функции + if not tool_calls and "name" in content_str and "db_" in content_str: + try: + start_idx = content_str.find("{") + end_idx = content_str.rfind("}") + 1 + if start_idx != -1 and end_idx != -1: + parsed = json.loads(content_str[start_idx:end_idx]) + if "name" in parsed: + tool_calls = [{"function": parsed}] + except Exception: + pass + + if tool_calls: + for tool in tool_calls: + fn_name = tool["function"]["name"] + fn_args = tool["function"].get("arguments", {}) + + formatted_text = handle_tool_call(fn_name, fn_args) + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history + + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": content_str} + ] + return content_str, updated_history + + except Exception as e: + return f"Ошибка обработки запроса: {e}", chat_history +''' + +# 6. Обновленный главный модуль llm_agent.py +LLM_AGENT_PY = '''""" +SCUD Orion AI — Главная точка входа для общения с LLM-ассистентом. +Перенаправляет вызовы в центральный оркестратор app.orchestrator. +""" + +from app.orchestrator import process_chat_message + +__all__ = ["process_chat_message"] +''' + +def build_structure(): + files = { + os.path.join(BASE_DIR, "app", "__init__.py"): "", + os.path.join(BASE_DIR, "app", "db", "__init__.py"): "", + os.path.join(BASE_DIR, "app", "db", "database.py"): DB_PY, + os.path.join(BASE_DIR, "app", "tools", "__init__.py"): "", + os.path.join(BASE_DIR, "app", "tools", "definitions.py"): TOOLS_DEFINITIONS_PY, + os.path.join(BASE_DIR, "app", "tools", "handlers.py"): TOOLS_HANDLERS_PY, + os.path.join(BASE_DIR, "app", "core", "__init__.py"): "", + os.path.join(BASE_DIR, "app", "core", "ollama_client.py"): OLLAMA_CLIENT_PY, + os.path.join(BASE_DIR, "app", "orchestrator.py"): ORCHESTRATOR_PY, + os.path.join(BASE_DIR, "llm_agent.py"): LLM_AGENT_PY, + } + + print("🚀 Начинаем рефакторинг проекта...") + for filepath, content in files.items(): + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, "w", encoding="utf-8") as f: + f.write(content.strip() + "\n") + print(f" [✓] Создан/обновлен: {filepath}") + + print("\n✅ Рефакторинг завершен успешно!") + +if __name__ == "__main__": + build_structure() \ No newline at end of file