diff --git a/llm_agent.py b/llm_agent.py index a60b9d5..867274b 100644 --- a/llm_agent.py +++ b/llm_agent.py @@ -1,4 +1,3 @@ -# === ANCHOR: IMPORTS_START === import sqlite3 import json import urllib.request @@ -6,14 +5,47 @@ import urllib.error from typing import List, Dict, Any, Tuple from datetime import datetime -DB_NAME = "context_memory.db" +# Абсолютный путь к базе данных проекта 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" -# === ANCHOR: IMPORTS_END === -# === ANCHOR: DB_TOOLS_START === -def db_get_tasks(status: str = None) -> str: +# === БЛОК РАБОТЫ С БАЗОЙ ДАННЫХ === + +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 @@ -24,10 +56,10 @@ def db_get_tasks(status: str = None) -> str: cursor.execute("SELECT * FROM tasks ORDER BY task_id") rows = [dict(r) for r in cursor.fetchall()] conn.close() - return json.dumps(rows, ensure_ascii=False) + return rows -def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> str: +def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: """Обновляет статус и/или срок выполнения задачи.""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() @@ -49,7 +81,7 @@ def db_update_task_status(task_id: str, status: str = None, due_date: str = None if not updates: conn.close() - return json.dumps({"error": "Не указаны параметры для обновления"}, ensure_ascii=False) + return {"error": "Не указаны параметры для обновления"} params.append(task_id.upper()) query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" @@ -57,50 +89,78 @@ def db_update_task_status(task_id: str, status: str = None, due_date: str = None if cursor.rowcount == 0: conn.close() - return json.dumps({"error": f"Задача {task_id} не найдена"}, ensure_ascii=False) + return {"error": f"Задача {task_id} не найдена"} conn.commit() conn.close() - return json.dumps({"status": "success", "message": f"Задача {task_id} обновлена"}, ensure_ascii=False) + return {"status": "success", "message": f"Задача {task_id} обновлена"} -def db_add_task(task_id: str, module: str, title: str, priority: str = "HIGH", due_date: str = None) -> str: - """Добавляет новую задачу в реестр.""" +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} создана"} + res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} except sqlite3.IntegrityError: res = {"error": f"Задача с ID {task_id} уже существует"} finally: conn.close() - return json.dumps(res, ensure_ascii=False) + return res -def db_get_rules() -> str: - """Возвращает действующие архитектурные правила.""" +def db_get_rules() -> list: + """Извлекает правила из таблицы ai_knowledge_base.""" conn = sqlite3.connect(DB_NAME) conn.row_factory = sqlite3.Row cursor = conn.cursor() - cursor.execute("SELECT category, rule_text FROM architecture_memory WHERE is_active = 1") + 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 json.dumps(rows, ensure_ascii=False) -# === ANCHOR: DB_TOOLS_END === + return rows -# === ANCHOR: TOOLS_SCHEMA_START === +# === СХЕМА ИНСТРУМЕНТОВ ДЛЯ OLLAMA === + TOOLS_SCHEMA = [ { "type": "function", "function": { "name": "db_get_tasks", - "description": "Получить список задач проекта с возможностью фильтрации по статусу (BACKLOG, IN_PROGRESS, COMPLETED).", + "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", "parameters": { "type": "object", "properties": { @@ -112,17 +172,62 @@ TOOLS_SCHEMA = [ } } }, + { + "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": "Изменить статус и/или срок выполнения (due_date) задачи в реестре.", + "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": "Срок выполнения задачи, например '2026-08-07 14:00'"} + "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"] } @@ -136,64 +241,73 @@ TOOLS_SCHEMA = [ "parameters": { "type": "object", "properties": { - "task_id": {"type": "string", "description": "Уникальный ID задачи, например TASK-18"}, - "module": {"type": "string", "description": "Модуль проекта, например services/scud_export"}, "title": {"type": "string", "description": "Краткое описание задачи"}, "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 14:00'"} + "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, + "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} }, - "required": ["task_id", "module", "title"] + "required": ["title"] } } - }, - { - "type": "function", - "function": { - "name": "db_get_rules", - "description": "Получить список архитектурных правил и ограничений проекта.", - "parameters": {"type": "object", "properties": {}} - } } ] -# === ANCHOR: TOOLS_SCHEMA_END === -# === ANCHOR: CHAT_AGENT_START === +# === ОСНОВНАЯ ФУНКЦИЯ ОБРАБОТКИ СООБЩЕНИЙ === + 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" - f"Текущая дата и время сервера: {current_now}.\n" - "Ты — ИИ-ассистент проекта SCUD Orion AI. " - "У тебя есть доступ к функциям для просмотра, добавления и изменения статусов и сроков задач (due_date) в БД. " - "Когда пользователь просит показать задачи, добавить новую или изменить статус — обязательно вызывай нужную функцию. " - "После вызова функции давай пользователю понятное, лаконичное резюме результата на русском языке. " - "При запросе списка всех задач не расписывай каждую подробно, а давай краткую сводку по количествам, " - "так как детали отображаются в выезжающем реестре задач." - ) + "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.2, # Низкая температура: ответы точные и без галлюцинаций - "top_p": 0.9, # Оптимальная выборка токенов - "num_gpu": 99 # Заставляет Ollama загрузить 100% слоев модели в VRAM + "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, @@ -207,86 +321,154 @@ def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = msg = res_data.get("message", {}) tool_calls = msg.get("tool_calls", []) - - # Резервная проверка, если Ollama прислала вызов функции текстом - content_str = msg.get("content", "") + content_str = msg.get("content", "").strip().replace("**", "") + + # Перехват вызова функции из текста if not tool_calls and "name" in content_str and "db_" in content_str: try: - parsed = json.loads(content_str) - if "name" in parsed: - tool_calls = [{"function": parsed}] + 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: - messages.append(msg) for tool in tool_calls: fn_name = tool["function"]["name"] fn_args = tool["function"].get("arguments", {}) + # 1. Вывод задач if fn_name == "db_get_tasks": - result = db_get_tasks(status=fn_args.get("status")) + 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": - result = 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") ) - elif fn_name == "db_add_task": - result = db_add_task( - task_id=fn_args.get("task_id"), - module=fn_args.get("module"), - title=fn_args.get("title"), - priority=fn_args.get("priority", "HIGH"), - due_date=fn_args.get("due_date") - ) - elif fn_name == "db_get_rules": - result = db_get_rules() - else: - result = json.dumps({"error": "Неизвестная функция"}) + + if "error" in res: + formatted_text = f"Ошибка обновления задачи: {res['error']}" + else: + formatted_text = f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" - messages.append({ - "role": "tool", - "content": result - }) + updated_history = chat_history + [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": formatted_text} + ] + return formatted_text, updated_history - # Вторым запросом передаем результат обратно модели для текстового ответа - payload = { - "model": MODEL_NAME, - "messages": messages, - "tools": TOOLS_SCHEMA, - "stream": False, - "options": { - "num_predict": 2048, # Достаточно для любых длинных списков и кода - "num_ctx": 8192, # Расширенная память диалога без потери скорости - "temperature": 0.2, # Низкая температура: ответы точные и без галлюцинаций - "top_p": 0.9, # Оптимальная выборка токенов - "num_gpu": 99 # Заставляет Ollama загрузить 100% слоев модели в VRAM - } - } - req_second = urllib.request.Request( - OLLAMA_URL, - data=json.dumps(payload_second).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) - with urllib.request.urlopen(req_second) as resp_second: - final_res = json.loads(resp_second.read().decode("utf-8")) - final_msg = final_res.get("message", {}).get("content", "") - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": final_msg} - ] - return final_msg, 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')} успешно удалена из базы!" - else: - final_msg = msg.get("content", "") - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": final_msg} - ] - return final_msg, updated_history + 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 -# === ANCHOR: CHAT_AGENT_END === \ No newline at end of file + return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history \ No newline at end of file diff --git a/static/index.html b/static/index.html index 1e2012e..c324ede 100644 --- a/static/index.html +++ b/static/index.html @@ -6,6 +6,21 @@ SCUD Orion AI — Context Manager + @@ -33,7 +48,7 @@ - +
@@ -41,16 +56,20 @@ ИИ-Ассистент

- Привет! Я подключен к вашей базе задач. Вы можете писать мне в чат или управлять задачами в реестре справа. + Привет! Я подключен к вашей базе задач и системным промптам. Вы можете писать команды прямо в чат или управлять задачами через панель справа.

+
-
- - @@ -58,12 +77,11 @@
- +