Compare commits

2 Commits
5 changed files with 1073 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# Виртуальное окружение Python
venv/
.venv/
env/
# Кэш Python
__pycache__/
*.py[cod]
*$py.class
# Локальная база данных SQLite
*.db
*.sqlite
*.sqlite3
# Переменные окружения и секреты
.env
# Логи и временные файлы
*.log
.DS_Store
.idea/
.vscode/
+78
View File
@@ -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()
+474
View File
@@ -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
+166
View File
@@ -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 ===
+332
View File
@@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SCUD Orion AI — Context Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Плавное (градиентное) исчезновение текста сверху при скролле */
.fade-scroll-top {
mask-image: linear-gradient(to bottom, transparent 0%, black 14px);
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 14px);
}
/* Скрытие стандартного скроллбара */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</head>
<body class="bg-slate-50 text-slate-800 h-screen flex flex-col font-sans">
<!-- Хедер -->
<header class="bg-white border-b border-slate-200 px-6 py-3 flex justify-between items-center shadow-sm">
<div class="flex items-center space-x-3">
<div class="bg-indigo-600 text-white p-2 rounded-lg">
<i class="fa-solid fa-brain text-lg"></i>
</div>
<div>
<h1 class="text-base font-bold leading-none text-slate-900">SCUD Orion AI</h1>
<span class="text-xs text-slate-500">Context & Task Manager</span>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex items-center text-xs text-emerald-600 font-medium bg-emerald-50 px-2.5 py-1 rounded-full border border-emerald-200">
<span class="h-2 w-2 rounded-full bg-emerald-500 mr-1.5"></span> API Online
</span>
<button onclick="toggleDrawer()" class="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 px-3.5 py-1.5 rounded-lg text-xs font-semibold flex items-center gap-2 transition border border-indigo-200">
<i class="fa-solid fa-list-check text-indigo-600"></i>
<span>Реестр задач</span>
<span id="task-count-badge" class="bg-indigo-600 text-white text-[10px] px-1.5 py-0.2 rounded-full">0</span>
</button>
</div>
</header>
<!-- Основная зона чата -->
<main class="flex-1 flex flex-col max-w-4xl w-full mx-auto bg-white my-4 rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div id="chat-window" class="flex-1 p-6 overflow-y-auto space-y-4 bg-slate-50/50">
<div class="bg-white border border-slate-200 rounded-xl p-4 max-w-2xl shadow-sm">
<p class="text-xs font-bold text-indigo-600 uppercase tracking-wider mb-1">
<i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент
</p>
<p class="text-slate-700 text-sm leading-relaxed">
Привет! Я подключен к вашей базе задач и системным промптам. Вы можете писать команды прямо в чат или управлять задачами через панель справа.
</p>
</div>
</div>
<!-- Поле ввода с авто-высотой, отключенным автокомплитом и сохраненной историей -->
<div class="p-4 bg-white border-t border-slate-200">
<form id="chat-form" onsubmit="sendMessage(event)" class="flex items-end gap-2">
<div class="flex-1 bg-slate-50 border border-slate-300 rounded-lg p-1 focus-within:border-indigo-600 focus-within:bg-white transition">
<textarea id="user-input" rows="1" autocomplete="off" autocorrect="off" spellcheck="false"
placeholder="Команда или вопрос (Shift+Enter — новая строка, ↑/↓ — история)..."
class="w-full bg-transparent text-slate-800 px-3 py-1.5 text-sm focus:outline-none resize-none overflow-y-auto max-h-[96px] leading-relaxed fade-scroll-top no-scrollbar"></textarea>
</div>
<button type="submit" id="send-btn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-5 h-[42px] rounded-lg text-sm transition flex items-center gap-2 shrink-0">
<span>Отправить</span>
<i class="fa-solid fa-paper-plane text-xs"></i>
</button>
</form>
</div>
</main>
<!-- Выезжающая справа панель (Drawer) -->
<div id="drawer-backdrop" onclick="toggleDrawer()" class="fixed inset-0 bg-slate-900/30 backdrop-blur-sm hidden transition-opacity z-40"></div>
<aside id="task-drawer" class="fixed right-0 top-0 h-full w-[420px] bg-white border-l border-slate-200 shadow-2xl transform translate-x-full transition-transform duration-300 ease-in-out z-50 flex flex-col">
<div class="p-4 border-b border-slate-200 flex justify-between items-center bg-slate-50">
<h2 class="font-bold text-slate-800 flex items-center gap-2 text-sm">
<i class="fa-solid fa-list-check text-indigo-600"></i> Реестр задач
</h2>
<div class="flex items-center gap-2">
<button onclick="loadTasks()" class="text-xs text-slate-500 hover:text-indigo-600 transition p-1" title="Обновить">
<i class="fa-solid fa-rotate-right"></i>
</button>
<button onclick="toggleDrawer()" class="text-slate-400 hover:text-slate-700 transition p-1">
<i class="fa-solid fa-xmark text-lg"></i>
</button>
</div>
</div>
<!-- Фильтры задач -->
<div class="flex border-b border-slate-200 bg-white px-2 pt-2 text-xs font-medium text-slate-500 gap-1">
<button onclick="setFilter('ALL')" id="filter-ALL" class="px-3 py-1.5 rounded-t-lg border-b-2 border-indigo-600 text-indigo-600 font-bold">Все</button>
<button onclick="setFilter('IN_PROGRESS')" id="filter-IN_PROGRESS" class="px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700">В работе</button>
<button onclick="setFilter('BACKLOG')" id="filter-BACKLOG" class="px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700">Бэклог</button>
<button onclick="setFilter('COMPLETED')" id="filter-COMPLETED" class="px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700">Завершено</button>
</div>
<div id="tasks-container" class="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/50">
<div class="text-center text-slate-400 py-8 text-xs">Загрузка задач...</div>
</div>
</aside>
<script>
const API_TOKEN = "scud_secret_token_2026";
const SESSION_ID = "web_session_main";
const STORAGE_KEY = 'scud_chat_input_history';
let currentFilter = 'ALL';
let allTasks = [];
// === ИСТОРИЯ КОМАНД (LOCALSTORAGE + СТРЕЛКИ ВВЕРХ/ВНИЗ) ===
let inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
let historyIndex = -1;
const userInputEl = document.getElementById('user-input');
if (userInputEl) {
// Динамическое расширение высоты поля до 4 строк (~96px)
userInputEl.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 96) + 'px';
});
userInputEl.addEventListener('keydown', function(e) {
// Отправка по Enter (без Shift)
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.getElementById('chat-form').requestSubmit();
}
// Стрелка ВВЕРХ — с первого нажатия забираем предыдущую команду
else if (e.key === 'ArrowUp') {
if (inputHistory.length > 0 && historyIndex < inputHistory.length - 1) {
e.preventDefault();
if (historyIndex === -1) {
this.dataset.draft = this.value;
}
historyIndex++;
this.value = inputHistory[inputHistory.length - 1 - historyIndex];
this.dispatchEvent(new Event('input'));
setTimeout(() => this.setSelectionRange(this.value.length, this.value.length), 0);
}
}
// Стрелка ВНИЗ — Навигация к более новым командам
else if (e.key === 'ArrowDown') {
if (historyIndex !== -1) {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
this.value = inputHistory[inputHistory.length - 1 - historyIndex];
} else {
historyIndex = -1;
this.value = this.dataset.draft || '';
}
this.dispatchEvent(new Event('input'));
setTimeout(() => this.setSelectionRange(this.value.length, this.value.length), 0);
}
}
});
}
function toggleDrawer() {
const drawer = document.getElementById('task-drawer');
const backdrop = document.getElementById('drawer-backdrop');
const isHidden = drawer.classList.contains('translate-x-full');
if (isHidden) {
drawer.classList.remove('translate-x-full');
backdrop.classList.remove('hidden');
} else {
drawer.classList.add('translate-x-full');
backdrop.classList.add('hidden');
}
}
function setFilter(status) {
currentFilter = status;
['ALL', 'IN_PROGRESS', 'BACKLOG', 'COMPLETED'].forEach(f => {
const btn = document.getElementById(`filter-${f}`);
if (f === status) {
btn.className = "px-3 py-1.5 rounded-t-lg border-b-2 border-indigo-600 text-indigo-600 font-bold";
} else {
btn.className = "px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700";
}
});
renderTasks();
}
async function loadTasks() {
try {
const res = await fetch('/api/v1/tasks', {
headers: { 'Authorization': `Bearer ${API_TOKEN}` }
});
allTasks = await res.json();
document.getElementById('task-count-badge').innerText = allTasks.length;
renderTasks();
} catch (err) {
document.getElementById('tasks-container').innerHTML = `<div class="text-red-500 text-xs py-4 text-center">Ошибка загрузки задач</div>`;
}
}
function renderTasks() {
const container = document.getElementById('tasks-container');
const filtered = allTasks.filter(t => currentFilter === 'ALL' || t.status === currentFilter);
if (filtered.length === 0) {
container.innerHTML = `<div class="text-slate-400 text-xs py-8 text-center">Нет задач с выбранным фильтром</div>`;
return;
}
container.innerHTML = filtered.map(t => {
let statusBadge = 'bg-slate-100 text-slate-600 border-slate-200';
let cardBg = 'bg-white';
if (t.status === 'COMPLETED') {
statusBadge = 'bg-emerald-50 text-emerald-700 border-emerald-300 font-semibold';
cardBg = 'bg-emerald-50/20';
} else if (t.status === 'IN_PROGRESS') {
statusBadge = 'bg-amber-50 text-amber-700 border-amber-300 font-bold';
cardBg = 'bg-amber-50/20 border-amber-200';
}
let priorityBadge = 'text-slate-500 bg-slate-100 border-slate-200';
if (t.priority === 'HIGH') priorityBadge = 'text-red-700 bg-red-50 border-red-200 font-bold';
let dueDateHtml = '';
if (t.due_date) {
dueDateHtml = `
<div class="mt-2 text-[11px] text-amber-800 bg-amber-50 border border-amber-200 px-2 py-0.5 rounded-md flex items-center gap-1.5 w-fit font-medium">
<i class="fa-solid fa-clock text-amber-600"></i>
<span>Срок: ${t.due_date}</span>
</div>
`;
}
return `
<div class="${cardBg} border border-slate-200 rounded-xl p-3.5 shadow-sm hover:shadow-md transition">
<div class="flex justify-between items-center mb-1.5">
<div class="flex items-center gap-1.5">
<span class="font-mono text-xs font-bold text-slate-900 bg-slate-100 px-2 py-0.5 rounded border border-slate-200">${t.task_id}</span>
<span class="text-[10px] uppercase px-1.5 py-0.5 rounded border ${priorityBadge}">${t.priority || 'HIGH'}</span>
</div>
<span class="text-[10px] uppercase px-2 py-0.5 rounded border ${statusBadge}">${t.status}</span>
</div>
<h3 class="text-xs font-semibold text-slate-800 mb-1 leading-snug">${t.title}</h3>
<div class="text-[10px] text-slate-400 font-mono flex items-center gap-1">
<i class="fa-solid fa-folder-closed text-slate-300"></i>
<span>${t.module}</span>
</div>
${dueDateHtml}
</div>
`;
}).join('');
}
async function sendMessage(e) {
e.preventDefault();
const input = document.getElementById('user-input');
const chatWindow = document.getElementById('chat-window');
const sendBtn = document.getElementById('send-btn');
const text = input.value.trim();
if (!text) return;
// Сохранение истории команд в localStorage (до 50 записей)
if (inputHistory[inputHistory.length - 1] !== text) {
inputHistory.push(text);
if (inputHistory.length > 50) inputHistory.shift();
localStorage.setItem(STORAGE_KEY, JSON.stringify(inputHistory));
}
historyIndex = -1;
chatWindow.innerHTML += `
<div class="flex justify-end">
<div class="bg-indigo-600 text-white rounded-xl px-4 py-2.5 max-w-2xl text-sm shadow-sm">
${text}
</div>
</div>
`;
input.value = '';
input.style.height = 'auto';
chatWindow.scrollTop = chatWindow.scrollHeight;
sendBtn.disabled = true;
sendBtn.classList.add('opacity-50');
try {
const res = await fetch('/api/v1/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`
},
body: JSON.stringify({ session_id: SESSION_ID, message: text })
});
const data = await res.json();
chatWindow.innerHTML += `
<div class="bg-white border border-slate-200 rounded-xl p-4 max-w-2xl shadow-sm">
<p class="text-xs font-bold text-indigo-600 uppercase tracking-wider mb-1"><i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент</p>
<p class="text-slate-700 text-sm whitespace-pre-wrap leading-relaxed">${data.reply}</p>
</div>
`;
chatWindow.scrollTop = chatWindow.scrollHeight;
loadTasks();
} catch (err) {
chatWindow.innerHTML += `
<div class="bg-red-50 border border-red-200 rounded-xl p-4 max-w-2xl text-red-700 text-sm">
Ошибка связи с сервером API.
</div>
`;
} finally {
sendBtn.disabled = false;
sendBtn.classList.remove('opacity-50');
}
}
loadTasks();
</script>
</body>
</html>