налажена работа с системными промптами (добавление, изменение, удаление). Восстановлена работа с задачами.

This commit is contained in:
2026-08-09 22:06:04 +03:00
parent 2fa151345f
commit 286355e4c0
5 changed files with 258 additions and 67 deletions
+12 -2
View File
@@ -1,7 +1,7 @@
import os import os
import sqlite3 import sqlite3
DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" DB_NAME = "/home/puh/scud_context_api/scud_orion_ai.db"
def init_db(): def init_db():
os.makedirs(os.path.dirname(DB_NAME), exist_ok=True) os.makedirs(os.path.dirname(DB_NAME), exist_ok=True)
@@ -39,9 +39,19 @@ def init_db():
); );
""") """)
# Добавление новой таблицы для сессионных состояний
cursor.execute("""
CREATE TABLE IF NOT EXISTS session_states (
session_id TEXT PRIMARY KEY,
state_type TEXT NOT NULL,
pending_data TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
conn.commit() conn.commit()
conn.close() conn.close()
print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно проверкой инициализирована!") print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно инициализирована!")
if __name__ == "__main__": if __name__ == "__main__":
init_db() init_db()
+70 -25
View File
@@ -1,6 +1,7 @@
import json import json
import urllib.request import urllib.request
import urllib.error import urllib.error
import logging
from typing import List, Dict, Any, Tuple from typing import List, Dict, Any, Tuple
from datetime import datetime from datetime import datetime
@@ -11,33 +12,48 @@ from .db_tools import (
db_update_task_status, db_update_task_status,
db_delete_task, db_delete_task,
db_add_task, db_add_task,
db_get_rules db_get_rules,
db_set_session_state,
db_get_session_state,
db_clear_session_state,
get_db_connection
) )
from .schemas import TOOLS_SCHEMA from .schemas import TOOLS_SCHEMA
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("SCUD_AGENT")
OLLAMA_URL = "http://192.168.11.3:11434/api/chat" OLLAMA_URL = "http://192.168.11.3:11434/api/chat"
MODEL_NAME = "qwen2.5:14b" MODEL_NAME = "qwen2.5:14b"
def format_rules_output(rules: List[Dict[str, Any]]) -> str: def clean_output(text: str) -> str:
if not rules: if not text:
return "База знаний пока пуста." return text
lines = [f"📚 База знаний и правила арбитража ({len(rules)}):\n"] artifacts = ["почемучто", "почто", "почему что"]
for idx, r in enumerate(rules, 1): lower_text = text.lower()
rule_text = r.get("rule_text", "").strip() for art in artifacts:
lines.append(f"{idx}. {rule_text}\n") if lower_text.startswith(art):
return "\n".join(lines).strip() text = text[len(art):].lstrip(",.!?:; -")
return text.strip()
def process_chat_message(user_id: int, user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: def process_chat_message(user_id: int, user_message: str, chat_history: List[Dict[str, Any]] = None, session_id: str = "web_session_main") -> Tuple[str, List[Dict[str, Any]]]:
if chat_history is None: if chat_history is None:
chat_history = [] chat_history = []
current_now = datetime.now().strftime("%Y-%m-%d %H:%M") logger.info(f"Получено сообщение от user_id={user_id}, session_id={session_id}: {user_message}")
# Запрос к локальной модели Qwen (полный цикл Function Calling) current_now = datetime.now().strftime("%Y-%m-%d %H:%M")
dynamic_prompt_text = db_get_active_system_prompt() dynamic_prompt_text = db_get_active_system_prompt()
session_state = db_get_session_state(session_id)
preview_status_note = ""
if session_state and session_state["state_type"] == "PROMPT_PREVIEW":
preview_status_note = "\n\n[АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение или отмена изменений пользователем]."
system_prompt = { system_prompt = {
"role": "system", "role": "system",
"content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" "content": f"Текущая дата и время сервера: {current_now}.\n\nТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ:\n{dynamic_prompt_text}{preview_status_note}"
} }
messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}]
@@ -47,7 +63,7 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic
"messages": messages, "messages": messages,
"tools": TOOLS_SCHEMA, "tools": TOOLS_SCHEMA,
"stream": False, "stream": False,
"options": {"num_predict": 2048, "num_ctx": 8192, "temperature": 0.1} "options": {"num_predict": 8192, "num_ctx": 8192, "temperature": 0.1}
} }
try: try:
@@ -60,6 +76,7 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic
res_data = json.loads(response.read().decode("utf-8")) res_data = json.loads(response.read().decode("utf-8"))
msg = res_data.get("message", {}) msg = res_data.get("message", {})
tool_calls = msg.get("tool_calls", []) tool_calls = msg.get("tool_calls", [])
logger.info(f"Ответ от Ollama получен. Tool calls: {bool(tool_calls)}")
if tool_calls: if tool_calls:
messages.append(msg) messages.append(msg)
@@ -67,6 +84,7 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic
for tool in tool_calls: for tool in tool_calls:
fn_name = tool["function"]["name"] fn_name = tool["function"]["name"]
fn_args = tool["function"].get("arguments", {}) fn_args = tool["function"].get("arguments", {})
logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}")
tool_result_content = "" tool_result_content = ""
if fn_name == "db_get_tasks": if fn_name == "db_get_tasks":
@@ -74,14 +92,41 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic
tool_result_content = json.dumps(tasks, ensure_ascii=False) tool_result_content = json.dumps(tasks, ensure_ascii=False)
elif fn_name in ["db_get_system_prompt", "db_get_system_prompts"]: elif fn_name in ["db_get_system_prompt", "db_get_system_prompts"]:
tool_result_content = db_get_active_system_prompt() prompt_content = db_get_active_system_prompt()
tool_result_content = json.dumps({"system_prompt": prompt_content}, ensure_ascii=False)
elif fn_name == "db_preview_prompt_merge":
proposed_text = fn_args.get("proposed_prompt", "")
if proposed_text:
# Фиксируем превью в сессии и сразу возвращаем текст пользователю на экран
db_set_session_state(session_id, "PROMPT_PREVIEW", proposed_text)
preview_reply = f"Ваше изменение успешно предпросмотрено. Полный обновленный системный промпт теперь выглядит так:\n\n{proposed_text}\n\nДля применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)."
return preview_reply, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": preview_reply}]
else:
tool_result_content = json.dumps({"status": "error", "message": "Текст превью пуст."}, ensure_ascii=False)
elif fn_name == "db_confirm_prompt_preview":
if session_state and session_state["state_type"] == "PROMPT_PREVIEW":
pending_text = session_state["pending_data"]
res = db_add_system_prompt("main_agent", pending_text)
db_clear_session_state(session_id)
tool_result_content = json.dumps(res, ensure_ascii=False)
else:
tool_result_content = json.dumps({"status": "error", "message": "Нет активного превью для подтверждения."}, ensure_ascii=False)
elif fn_name == "db_cancel_prompt_preview":
db_clear_session_state(session_id)
tool_result_content = json.dumps({"status": "success", "message": "Превью отменено."}, ensure_ascii=False)
elif fn_name == "db_add_system_prompt": elif fn_name == "db_add_system_prompt":
res = db_add_system_prompt( try:
name=fn_args.get("name", "main_agent"), prompt_text = fn_args.get("prompt_text") if isinstance(fn_args, dict) else str(fn_args)
prompt_text=fn_args.get("prompt_text") name = fn_args.get("name", "main_agent") if isinstance(fn_args, dict) else "main_agent"
) res = db_add_system_prompt(name=name, prompt_text=prompt_text)
db_clear_session_state(session_id)
tool_result_content = json.dumps(res, ensure_ascii=False) tool_result_content = json.dumps(res, ensure_ascii=False)
except Exception as e:
tool_result_content = json.dumps({"status": "error", "error": str(e)}, ensure_ascii=False)
elif fn_name == "db_get_rules": elif fn_name == "db_get_rules":
tool_result_content = json.dumps(db_get_rules(), ensure_ascii=False) tool_result_content = json.dumps(db_get_rules(), ensure_ascii=False)
@@ -103,12 +148,11 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic
"content": tool_result_content "content": tool_result_content
}) })
# Вторичный запрос модели для формирования итогового ответа оператору
second_payload = { second_payload = {
"model": MODEL_NAME, "model": MODEL_NAME,
"messages": messages, "messages": messages,
"stream": False, "stream": False,
"options": {"num_predict": 2048, "num_ctx": 8192, "temperature": 0.1} "options": {"num_predict": 8192, "num_ctx": 8192, "temperature": 0.1}
} }
sec_req = urllib.request.Request( sec_req = urllib.request.Request(
OLLAMA_URL, OLLAMA_URL,
@@ -117,11 +161,12 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic
) )
with urllib.request.urlopen(sec_req) as sec_response: with urllib.request.urlopen(sec_req) as sec_response:
sec_res_data = json.loads(sec_response.read().decode("utf-8")) sec_res_data = json.loads(sec_response.read().decode("utf-8"))
final_content = sec_res_data.get("message", {}).get("content", "").strip().replace("**", "") final_content = clean_output(sec_res_data.get("message", {}).get("content", "").strip().replace("**", ""))
return final_content, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": final_content}] return final_content, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": final_content}]
content_str = msg.get("content", "").strip().replace("**", "") content_str = clean_output(msg.get("content", "").strip().replace("**", ""))
return content_str or "Запрос обработан.", chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": content_str}] return content_str or "Запрос обработан.", chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": content_str}]
except urllib.error.URLError as e: except Exception as ex:
return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history logger.exception(f"Непредвиденная ошибка: {ex}")
return f"Внутренняя ошибка сервера: {ex}", chat_history
+60 -12
View File
@@ -1,11 +1,20 @@
import sqlite3 import sqlite3
import logging
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
DB_PATH = "/home/puh/scud_context_api/scud_orion_ai.db" # Настройка логирования
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("DB_TOOLS")
DB_PATH = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db"
def get_db_connection(): def get_db_connection():
conn = sqlite3.connect(DB_PATH) # Увеличиваем таймаут до 30 секунд, чтобы соединения ожидали завершения соседних транзакций,
# а также включаем WAL-режим для безопасного параллельного чтения и записи.
conn = sqlite3.connect(DB_PATH, timeout=30.0)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL;")
conn.execute("PRAGMA synchronous = NORMAL;")
return conn return conn
def normalize_task_id(task_id_input: str) -> str: def normalize_task_id(task_id_input: str) -> str:
@@ -107,26 +116,36 @@ def db_get_active_system_prompt() -> str:
return row["prompt_text"] if row else "Ты — ИИ-ассистент SCUD Orion AI." return row["prompt_text"] if row else "Ты — ИИ-ассистент SCUD Orion AI."
def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]: def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]:
conn = get_db_connection() try:
with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("BEGIN IMMEDIATE;")
# Проверяем наличие промпта с таким именем, чтобы не плодить мусор # Проверяем, существует ли уже промпт с таким именем
cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,)) cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,))
existing = cursor.fetchone() existing = cursor.fetchone()
if existing: if existing:
cursor.execute(""" # Обновляем существующий активный промпт
UPDATE system_prompts cursor.execute(
SET prompt_text = ?, updated_at = CURRENT_TIMESTAMP, is_active = 1 "UPDATE system_prompts SET prompt_text = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE name = ?",
WHERE name = ? (prompt_text, name)
""", (prompt_text, name)) )
else: else:
cursor.execute("UPDATE system_prompts SET is_active = 0") # Вставляем новый, если его не было
cursor.execute("INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", (name, prompt_text)) cursor.execute(
"INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)",
(name, prompt_text)
)
conn.commit() conn.commit()
conn.close()
logger.info("Системный промпт успешно сохранен и применен в базе данных.")
return {"status": "success", "message": "Системный промпт успешно обновлен"} return {"status": "success", "message": "Системный промпт успешно обновлен"}
except Exception as e:
logger.error(f"Ошибка при сохранении промпта в БД: {e}")
return {"status": "error", "error": str(e)}
def db_get_rules() -> List[Dict[str, Any]]: def db_get_rules() -> List[Dict[str, Any]]:
conn = get_db_connection() conn = get_db_connection()
@@ -135,3 +154,32 @@ def db_get_rules() -> List[Dict[str, Any]]:
rows = cursor.fetchall() rows = cursor.fetchall()
conn.close() conn.close()
return [dict(r) for r in rows] return [dict(r) for r in rows]
def db_set_session_state(session_id: str, state_type: str, data: str):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO session_states (session_id, state_type, pending_data, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(session_id) DO UPDATE SET
state_type = excluded.state_type,
pending_data = excluded.pending_data,
updated_at = CURRENT_TIMESTAMP
""", (session_id, state_type, data))
conn.commit()
conn.close()
def db_get_session_state(session_id: str) -> Optional[Dict[str, Any]]:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT state_type, pending_data FROM session_states WHERE session_id = ?", (session_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def db_clear_session_state(session_id: str):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM session_states WHERE session_id = ?", (session_id,))
conn.commit()
conn.close()
+33 -15
View File
@@ -31,21 +31,6 @@ TOOLS_SCHEMA = [
"parameters": {"type": "object", "properties": {}} "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", "type": "function",
"function": { "function": {
@@ -92,5 +77,38 @@ TOOLS_SCHEMA = [
"required": ["title"] "required": ["title"]
} }
} }
},
{
"type": "function",
"function": {
"name": "db_confirm_prompt_preview",
"description": "Подтвердить и сохранить текущее подготовленное превью в БД. Вызывай этот инструмент, когда пользователь говорит 'подтверждаю', 'да', 'вноси', 'применяй', 'сохраняй' или одобряет превью в любой форме.",
"parameters": {"type": "object", "properties": {}}
}
},
{
"type": "function",
"function": {
"name": "db_cancel_prompt_preview",
"description": "Отменить текущее превью системного промпта и сбросить изменения. Вызывай, когда пользователь явно отказывается от изменений.",
"parameters": {"type": "object", "properties": {}}
}
},
{
"type": "function",
"function": {
"name": "db_preview_prompt_merge",
"description": "ОБЯЗАТЕЛЬНЫЙ ИНСТРУМЕНТ для ЛЮБЫХ изменений системного промпта (добавление пунктов, удаление, форматирование, отступы). Вызывай его ВСЕГДА, когда пользователь просит изменить промпт. В параметре proposed_prompt передавай ИТОГОВЫЙ полный текст со всеми разделами целиком.",
"parameters": {
"type": "object",
"properties": {
"proposed_prompt": {
"type": "string",
"description": "Полный текст системного промпта, содержащий все разделы от 1 до 3 с учетом внесенных изменений."
}
},
"required": ["proposed_prompt"]
}
}
} }
] ]
+70
View File
@@ -0,0 +1,70 @@
python3 -c "
import sqlite3
db_path = '/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db'
full_prompt = '''Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI (система учета рабочего времени и контроля доступа).
1. РОЛЬ И ЗАДАЧИ АССИСТЕНТА
1.1. Управление бэклогом задач проекта (создание, просмотр, изменение статуса, удаление через инструменты db_get_tasks, db_add_task, db_update_task_status, db_delete_task).
1.2. Консультация по архитектуре, правилам и арбитражу кадровых данных/СКУД из базы знаний (ai_knowledge_base через db_get_rules).
1.3. Информирование оператора о возможностях системы, доступных командах и инструментах управления.
1.4. Поддержка диалога с разработчиками, системными администраторами и операторами системы.
2. ПРАВИЛА АНАЛИЗА И ВЫЗОВА ИНСТРУМЕНТОВ
2.1. Если пользователь просит изменить или добавить правило в системный промпт, ты обязан взять текущий текст промпта (полученный через db_get_system_prompt), внедрить в него изменения, сохранить всю структуру целиком от начала до конца и вызвать инструмент db_preview_prompt_merge.
2.2. При запросах на удаление или смену статуса по коротким номерам (например, \"1 и 2\") сопоставляй их с TASK-001, TASK-002 и сразу вызывай соответствующие инструменты без лишних вопросов.
2.3. Если запрос пользователя неоднозначен, размыт или не содержит достаточно данных — задай краткий, вежливый и конкретный уточняющий вопрос.
3. ПРАВИЛА ФОРМАТИРОВАНИЯ И СТИЛЯ
3.1. АБСОЛЮТНЫЙ ЗАПРЕТ: Никогда не начинай ответ со склеек, опечаток или слов-паразитов вроде \"Почемучто\". Ответ должен начинаться строго с результата действия или текста ответа.
3.2. По умолчанию отвечай строго в чистом текстовом формате (plain text). Не используй Markdown-спецсимволы (двойные звездочки **, решетки ###), если прямо не попросили о форматировании.
3.3. Сохраняй инженерный, лаконичный и профессиональный стиль.'''
3.4. ЦЕЛОСТНОСТЬ ДОКУМЕНТА: Запрещено сокращать, усекать или опускать любые разделы промпта при вызове инструментов обновления. Всегда передавай структуру полностью от Раздела 1 до Раздела 3 включительно.
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('DELETE FROM system_prompts WHERE name = ?', ('main_agent',))
cursor.execute('INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)', ('main_agent', full_prompt))
conn.commit()
conn.close()
print('[✓] Полный промпт успешно восстановлен в БД.')
"
python3 -c '
import sqlite3
from passlib.context import CryptContext
db_path = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db"
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 1. Создаем таблицу пользователей, если ее не было
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER DEFAULT 0,
full_name TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
# 2. Создаем хэш пароля (например, для пароля "admin" или вашего текущего)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Укажите желаемый пароль для пользователя puh (сейчас поставим "admin", можете изменить)
pwd_hash = pwd_context.hash("M@n0raga")
# 3. Вставляем или обновляем пользователя puh
cursor.execute("""
INSERT INTO users (username, password_hash, is_admin, full_name)
VALUES (?, ?, 1, ?)
ON CONFLICT(username) DO UPDATE SET
password_hash = excluded.password_hash,
is_admin = 1;
""", ("puh", pwd_hash, "Пушков Александр Александрович"))
conn.commit()
conn.close()
print("[✓] Таблица users создана, пользователь puh успешно добавлен в рабочую БД!")
'