diff --git a/main.py b/main.py index 67dad4d..5abcee8 100644 --- a/main.py +++ b/main.py @@ -1,166 +1,73 @@ -# === ANCHOR: IMPORTS_START === -import os import json -import sqlite3 -from typing import List, Optional, Dict, Any - -from fastapi import FastAPI, HTTPException, Depends, Security +import urllib.request +from typing import List, Dict, Any, Optional +from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse, HTMLResponse +from fastapi.responses import FileResponse from pydantic import BaseModel from llm.agent import process_chat_message -# === ANCHOR: IMPORTS_END === +from llm.db_tools import db_get_tasks +API_TOKEN = "scud_secret_token_2026" +OLLAMA_URL = "http://192.168.11.3:11434/api/chat" +MODEL_NAME = "qwen2.5:14b" -# === ANCHOR: APP_INIT_START === -app = FastAPI( - title="SCUD Orion AI Context & Task Tracker API", - version="2.0.0", - description="REST API локального контекста, памяти и трекинга задач для ИИ-аудитора" -) - -DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.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="Недействительный токен доступа") +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + if credentials.credentials != API_TOKEN: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Неверный токен доступа", + headers={"WWW-Authenticate": "Bearer"}, + ) return credentials.credentials +app = FastAPI(title="SCUD Orion AI Context API") -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) +class ChatRequest(BaseModel): + session_id: str + message: str + +@app.get("/") def read_root(): return FileResponse("static/index.html") -# === ANCHOR: STATIC_ROUTES_END === +@app.get("/api/v1/tasks") +def get_tasks(token: str = Depends(verify_token)): + return db_get_tasks() -# === 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/chat") +def chat_endpoint(req: ChatRequest, token: str = Depends(verify_token)): + reply, _ = process_chat_message(req.message) + return {"reply": reply} - -@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() +# Эндпоинт для гостевого режима (без авторизации и без привязки к проекту) +@app.post("/api/v1/chat/guest") +def guest_chat_endpoint(req: ChatRequest): + payload = { + "model": MODEL_NAME, + "messages": [ + {"role": "system", "content": "Ты — полезный ИИ-ассистент. Отвечай на вопросы пользователя четко и по существу."}, + {"role": "user", "content": req.message} + ], + "stream": False, + "options": {"num_predict": 2048, "temperature": 0.3} + } - 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 + try: + req_ollama = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + with urllib.request.urlopen(req_ollama) as response: + res_data = json.loads(response.read().decode("utf-8")) + reply = res_data.get("message", {}).get("content", "").strip() + return {"reply": reply} + except Exception as e: + return {"reply": f"Ошибка связи с локальной нейросетью: {e}"} diff --git a/static/index.html b/static/index.html index 4f48abe..e92b28c 100644 --- a/static/index.html +++ b/static/index.html @@ -2,98 +2,147 @@ - + SCUD Orion AI — Context Manager - + + + +
+
+
+
+ +
+
+

SCUD Orion AI

+

Авторизация в системе

+
+
+ +
+
+ + +
+ + + + +
+ +
+
+
Или
+
+ + +
+
-
-
-
- +
+
+
+
-
-

SCUD Orion AI

- +
+

SCUD Orion AI

+ Task & Context API
-
- - API Online +
+ - +
- -
-
-
+ +
+
+

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

-

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

+ Привет! Задавайте вопросы нейросети прямо в чат.

-
-
-
+
+ +
+ class="w-full bg-transparent text-slate-900 text-sm focus:outline-none resize-none overflow-y-auto max-h-[80px] leading-normal no-scrollbar">
-
-
+
- + + + + + - + \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index ef697d0..f03496d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,11 +1,12 @@ -const API_TOKEN = "scud_secret_token_2026"; +const AUTH_TOKEN_KEY = "scud_api_auth_token"; const SESSION_ID = "web_session_main"; const STORAGE_KEY = 'scud_chat_input_history'; +let API_TOKEN = localStorage.getItem(AUTH_TOKEN_KEY) || ""; +let IS_GUEST = localStorage.getItem('scud_is_guest') === 'true'; let currentFilter = 'ALL'; let allTasks = []; -// === ИСТОРИЯ КОМАНД (LOCALSTORAGE + СТРЕЛКИ ВВЕРХ/ВНИЗ) === let inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); let historyIndex = -1; @@ -13,10 +14,9 @@ document.addEventListener('DOMContentLoaded', () => { 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'; + this.style.height = Math.min(this.scrollHeight, 80) + 'px'; }); userInputEl.addEventListener('keydown', function(e) { @@ -53,163 +53,20 @@ document.addEventListener('DOMContentLoaded', () => { }); } - loadTasks(); -}); - -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'); + if (IS_GUEST) { + hideAuthModal(); + updateUIState(); + } else if (API_TOKEN) { + verifyToken(API_TOKEN).then(isValid => { + if (isValid) { + hideAuthModal(); + updateUIState(); + loadTasks(); + } else { + showAuthModal(); + } + }); } else { - drawer.classList.add('translate-x-full'); - backdrop.classList.add('hidden'); + showAuthModal(); } -} - -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 = `
Ошибка загрузки задач
`; - } -} - -function renderTasks() { - const container = document.getElementById('tasks-container'); - const filtered = allTasks.filter(t => currentFilter === 'ALL' || t.status === currentFilter); - - if (filtered.length === 0) { - container.innerHTML = `
Нет задач с выбранным фильтром
`; - 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 = ` -
- - Срок: ${t.due_date} -
- `; - } - - return ` -
-
-
- ${t.task_id} - ${t.priority || 'HIGH'} -
- ${t.status} -
- -

${t.title}

- -
- - ${t.module} -
- - ${dueDateHtml} -
- `; - }).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; - - 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 += ` -
-
- ${text} -
-
- `; - 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 += ` -
-

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

-

${data.reply}

-
- `; - chatWindow.scrollTop = chatWindow.scrollHeight; - loadTasks(); - - } catch (err) { - chatWindow.innerHTML += ` -
- Ошибка связи с сервером API. -
- `; - } finally { - sendBtn.disabled = false; - sendBtn.classList.remove('opacity-50'); - } -} +}); diff --git a/static/js/auth.js b/static/js/auth.js new file mode 100644 index 0000000..2e56bf9 --- /dev/null +++ b/static/js/auth.js @@ -0,0 +1,72 @@ +function showAuthModal() { + document.getElementById('auth-modal').classList.remove('hidden'); +} + +function hideAuthModal() { + document.getElementById('auth-modal').classList.add('hidden'); +} + +async function verifyToken(token) { + try { + const res = await fetch('/api/v1/tasks', { + headers: { 'Authorization': `Bearer ${token}` } + }); + return res.status === 200; + } catch (err) { + return false; + } +} + +async function handleLogin(e) { + e.preventDefault(); + const tokenInput = document.getElementById('auth-token-input'); + const errorEl = document.getElementById('auth-error'); + const token = tokenInput.value.trim(); + + if (!token) return; + + errorEl.classList.add('hidden'); + const isValid = await verifyToken(token); + + if (isValid) { + API_TOKEN = token; + IS_GUEST = false; + localStorage.setItem(AUTH_TOKEN_KEY, token); + localStorage.removeItem('scud_is_guest'); + hideAuthModal(); + updateUIState(); + loadTasks(); + } else { + errorEl.classList.remove('hidden'); + } +} + +function enableGuestMode() { + IS_GUEST = true; + API_TOKEN = ""; + localStorage.setItem('scud_is_guest', 'true'); + hideAuthModal(); + updateUIState(); +} + +function logout() { + localStorage.removeItem(AUTH_TOKEN_KEY); + localStorage.removeItem('scud_is_guest'); + API_TOKEN = ""; + IS_GUEST = false; + document.getElementById('auth-token-input').value = ""; + showAuthModal(); +} + +function updateUIState() { + const tasksBtn = document.getElementById('tasks-drawer-btn'); + const guestBadge = document.getElementById('guest-badge'); + + if (IS_GUEST) { + if (tasksBtn) tasksBtn.classList.add('hidden'); + if (guestBadge) guestBadge.classList.remove('hidden'); + } else { + if (tasksBtn) tasksBtn.classList.remove('hidden'); + if (guestBadge) guestBadge.classList.add('hidden'); + } +} diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..83f8d28 --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,71 @@ +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; + + 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 += ` +
+
+ ${text} +
+
+ `; + input.value = ''; + input.style.height = 'auto'; + chatWindow.scrollTop = chatWindow.scrollHeight; + + sendBtn.disabled = true; + sendBtn.classList.add('opacity-50'); + + const endpoint = IS_GUEST ? '/api/v1/chat/guest' : '/api/v1/chat'; + const headers = { 'Content-Type': 'application/json' }; + if (!IS_GUEST) { + headers['Authorization'] = `Bearer ${API_TOKEN}`; + } + + try { + const res = await fetch(endpoint, { + method: 'POST', + headers: headers, + body: JSON.stringify({ session_id: SESSION_ID, message: text }) + }); + + if (res.status === 401 && !IS_GUEST) { + logout(); + return; + } + + const data = await res.json(); + const assistantTitle = IS_GUEST ? "Локальная нейросеть (Гость)" : "ИИ-Ассистент SCUD Orion AI"; + + chatWindow.innerHTML += ` +
+

${assistantTitle}

+

${data.reply}

+
+ `; + chatWindow.scrollTop = chatWindow.scrollHeight; + if (!IS_GUEST) loadTasks(); + + } catch (err) { + chatWindow.innerHTML += ` +
+ Ошибка связи с сервером. +
+ `; + } finally { + sendBtn.disabled = false; + sendBtn.classList.remove('opacity-50'); + } +} diff --git a/static/js/tasks.js b/static/js/tasks.js new file mode 100644 index 0000000..ae10b4e --- /dev/null +++ b/static/js/tasks.js @@ -0,0 +1,93 @@ +function toggleDrawer() { + if (IS_GUEST) return; + 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 (btn) { + btn.className = (f === status) + ? "px-3 py-1.5 rounded-t-lg border-b-2 border-indigo-600 text-indigo-600 font-bold whitespace-nowrap" + : "px-3 py-1.5 rounded-t-lg border-b-2 border-transparent whitespace-nowrap"; + } + }); + renderTasks(); +} + +async function loadTasks() { + if (IS_GUEST || !API_TOKEN) return; + try { + const res = await fetch('/api/v1/tasks', { + headers: { 'Authorization': `Bearer ${API_TOKEN}` } + }); + if (res.status === 401) { + logout(); + return; + } + allTasks = await res.json(); + document.getElementById('task-count-badge').innerText = allTasks.length; + renderTasks(); + } catch (err) { + document.getElementById('tasks-container').innerHTML = `
Ошибка загрузки задач
`; + } +} + +function renderTasks() { + const container = document.getElementById('tasks-container'); + const filtered = allTasks.filter(t => currentFilter === 'ALL' || t.status === currentFilter); + + if (filtered.length === 0) { + container.innerHTML = `
Нет задач с выбранным фильтром
`; + 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 = t.due_date ? ` +
+ + Срок: ${t.due_date} +
` : ''; + + return ` +
+
+
+ ${t.task_id} + ${t.priority || 'HIGH'} +
+ ${t.status} +
+

${t.title}

+
+ + ${t.module} +
+ ${dueDateHtml} +
+ `; + }).join(''); +}