+ ИИ-Ассистент +
++ Привет! Я подключен к вашей базе задач. Вы можете писать мне в чат или управлять задачами в реестре справа. +
+diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4eae693 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Виртуальное окружение Python +venv/ +.venv/ +env/ + +# Кэш Python +__pycache__/ +*.py[cod] +*$py.class + +# Локальная база данных SQLite +*.db +*.sqlite +*.sqlite3 + +# Переменные окружения и секреты +.env + +# Логи и временные файлы +*.log +.DS_Store +.idea/ +.vscode/ \ 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..a60b9d5 --- /dev/null +++ b/llm_agent.py @@ -0,0 +1,292 @@ +# === ANCHOR: IMPORTS_START === +import sqlite3 +import json +import urllib.request +import urllib.error +from typing import List, Dict, Any, Tuple +from datetime import datetime + +DB_NAME = "context_memory.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: + """Извлекает список задач из реестра БД.""" + 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 json.dumps(rows, ensure_ascii=False) + + +def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> str: + """Обновляет статус и/или срок выполнения задачи.""" + 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 json.dumps({"error": "Не указаны параметры для обновления"}, ensure_ascii=False) + + 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 json.dumps({"error": f"Задача {task_id} не найдена"}, ensure_ascii=False) + + conn.commit() + conn.close() + return json.dumps({"status": "success", "message": f"Задача {task_id} обновлена"}, ensure_ascii=False) + + +def db_add_task(task_id: str, module: str, title: str, priority: str = "HIGH", due_date: str = None) -> str: + """Добавляет новую задачу в реестр.""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + 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} создана"} + except sqlite3.IntegrityError: + res = {"error": f"Задача с ID {task_id} уже существует"} + finally: + conn.close() + return json.dumps(res, ensure_ascii=False) + + +def db_get_rules() -> str: + """Возвращает действующие архитектурные правила.""" + 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") + rows = [dict(r) for r in cursor.fetchall()] + conn.close() + return json.dumps(rows, ensure_ascii=False) +# === ANCHOR: DB_TOOLS_END === + + +# === ANCHOR: TOOLS_SCHEMA_START === +TOOLS_SCHEMA = [ + { + "type": "function", + "function": { + "name": "db_get_tasks", + "description": "Получить список задач проекта с возможностью фильтрации по статусу (BACKLOG, IN_PROGRESS, COMPLETED).", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." + } + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_update_task_status", + "description": "Изменить статус и/или срок выполнения (due_date) задачи в реестре.", + "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'"} + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "db_add_task", + "description": "Добавить новую задачу в бэклог проекта.", + "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'"} + }, + "required": ["task_id", "module", "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") + + system_prompt = { + "role": "system", + "content": ( + f"Текущая дата и время сервера: {current_now}.\n" + f"Текущая дата и время сервера: {current_now}.\n" + "Ты — ИИ-ассистент проекта SCUD Orion AI. " + "У тебя есть доступ к функциям для просмотра, добавления и изменения статусов и сроков задач (due_date) в БД. " + "Когда пользователь просит показать задачи, добавить новую или изменить статус — обязательно вызывай нужную функцию. " + "После вызова функции давай пользователю понятное, лаконичное резюме результата на русском языке. " + "При запросе списка всех задач не расписывай каждую подробно, а давай краткую сводку по количествам, " + "так как детали отображаются в выезжающем реестре задач." + ) + } + + 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 + } +} + + 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", []) + + # Резервная проверка, если Ollama прислала вызов функции текстом + content_str = msg.get("content", "") + 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}] + 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", {}) + + if fn_name == "db_get_tasks": + result = db_get_tasks(status=fn_args.get("status")) + elif fn_name == "db_update_task_status": + result = 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": "Неизвестная функция"}) + + messages.append({ + "role": "tool", + "content": result + }) + + # Вторым запросом передаем результат обратно модели для текстового ответа + 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 + + 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 + + except urllib.error.URLError as e: + return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history +# === ANCHOR: CHAT_AGENT_END === \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..89c2d0c --- /dev/null +++ b/main.py @@ -0,0 +1,166 @@ +# === ANCHOR: IMPORTS_START === +import os +import json +import sqlite3 +from typing import List, Optional, Dict, Any + +from fastapi import FastAPI, HTTPException, Depends, Security +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, HTMLResponse +from pydantic import BaseModel + +from llm_agent import process_chat_message +# === ANCHOR: IMPORTS_END === + + +# === ANCHOR: APP_INIT_START === +app = FastAPI( + title="SCUD Orion AI Context & Task Tracker API", + version="2.0.0", + description="REST API локального контекста, памяти и трекинга задач для ИИ-аудитора" +) + +DB_NAME = "context_memory.db" +SECURITY_TOKEN = os.getenv("API_BEARER_TOKEN", "scud_secret_token_2026") +security = HTTPBearer() +# === ANCHOR: APP_INIT_END === + + +# === ANCHOR: DATABASE_HELPERS_START === +def init_chat_table(): + """Создает таблицу истории чата, если она не существует.""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS chat_sessions ( + session_id TEXT PRIMARY KEY, + history_json TEXT NOT NULL, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + """) + conn.commit() + conn.close() + +init_chat_table() + + +def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)): + if credentials.credentials != SECURITY_TOKEN: + raise HTTPException(status_code=403, detail="Недействительный токен доступа") + return credentials.credentials + + +def get_db_connection(): + conn = sqlite3.connect(DB_NAME) + conn.row_factory = sqlite3.Row + return conn +# === ANCHOR: DATABASE_HELPERS_END === + + +# === ANCHOR: SCHEMAS_START === +class TaskSchema(BaseModel): + task_id: str + module: str + title: str + status: str + priority: str + completed_at: Optional[str] = None + + +class RuleSchema(BaseModel): + id: int + category: str + rule_text: str + is_active: int + + +class ChatRequest(BaseModel): + session_id: str = "default" + message: str + + +class ChatResponse(BaseModel): + session_id: str + reply: str +# === ANCHOR: SCHEMAS_END === + + +# === ANCHOR: STATIC_ROUTES_START === +# Подключаем папку static для отдачи HTML-интерфейса +app.mount("/static", StaticFiles(directory="static"), name="static") + +@app.get("/", response_class=HTMLResponse) +def read_root(): + return FileResponse("static/index.html") +# === ANCHOR: STATIC_ROUTES_END === + + +# === ANCHOR: TASKS_ENDPOINTS_START === +@app.get("/api/v1/tasks", response_model=List[TaskSchema], dependencies=[Depends(verify_token)]) +def get_all_tasks(status: Optional[str] = None): + conn = get_db_connection() + 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 = cursor.fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/v1/tasks/{task_id}/complete", dependencies=[Depends(verify_token)]) +def mark_task_completed(task_id: str): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute( + "UPDATE tasks SET status = 'COMPLETED', completed_at = CURRENT_TIMESTAMP WHERE task_id = ?", + (task_id.upper(),) + ) + if cursor.rowcount == 0: + conn.close() + raise HTTPException(status_code=404, detail=f"Задача {task_id} не найдена") + conn.commit() + conn.close() + return {"status": "success", "message": f"Задача {task_id} успешно выполнена"} +# === ANCHOR: TASKS_ENDPOINTS_END === + + +# === ANCHOR: RULES_AND_CHAT_ENDPOINTS_START === +@app.get("/api/v1/rules", response_model=List[RuleSchema], dependencies=[Depends(verify_token)]) +def get_active_rules(): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT * FROM architecture_memory WHERE is_active = 1 ORDER BY id") + rows = cursor.fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/v1/chat", response_model=ChatResponse, dependencies=[Depends(verify_token)]) +def chat_endpoint(req: ChatRequest): + """Принимает сообщение, подтягивает историю из БД, выполняет вызовы функций к БД и сохраняет историю.""" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT history_json FROM chat_sessions WHERE session_id = ?", (req.session_id,)) + row = cursor.fetchone() + + chat_history = json.loads(row["history_json"]) if row else [] + + reply, updated_history = process_chat_message(req.message, chat_history) + + cursor.execute( + "INSERT OR REPLACE INTO chat_sessions (session_id, history_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", + (req.session_id, json.dumps(updated_history, ensure_ascii=False)) + ) + conn.commit() + conn.close() + + return ChatResponse(session_id=req.session_id, reply=reply) + + +@app.get("/health") +def health_check(): + return {"status": "healthy", "service": "scud_context_api", "version": "2.0.0"} +# === ANCHOR: RULES_AND_CHAT_ENDPOINTS_END === \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..1e2012e --- /dev/null +++ b/static/index.html @@ -0,0 +1,266 @@ + + +
+ + ++ ИИ-Ассистент +
++ Привет! Я подключен к вашей базе задач. Вы можете писать мне в чат или управлять задачами в реестре справа. +
+