Spaces:
Runtime error
Runtime error
Upload 3 files
Browse files- START.bat +29 -0
- app.py +1005 -0
- requirements.txt +9 -0
START.bat
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo ================================================
|
| 3 |
+
echo Manager Intelligence Agent - Local Setup
|
| 4 |
+
echo ================================================
|
| 5 |
+
echo.
|
| 6 |
+
echo [1/3] Installing Python packages...
|
| 7 |
+
pip install -r requirements.txt
|
| 8 |
+
if %errorlevel% neq 0 (
|
| 9 |
+
echo ERROR: pip install failed. Check requirements.txt exists.
|
| 10 |
+
pause
|
| 11 |
+
exit /b 1
|
| 12 |
+
)
|
| 13 |
+
echo.
|
| 14 |
+
echo [2/3] Checking Ollama...
|
| 15 |
+
curl -s http://localhost:11434/api/tags >nul 2>&1
|
| 16 |
+
if %errorlevel% == 0 (
|
| 17 |
+
echo Ollama is running!
|
| 18 |
+
) else (
|
| 19 |
+
echo WARNING: Ollama not running. Start Ollama first.
|
| 20 |
+
echo Download from: https://ollama.com
|
| 21 |
+
echo After installing run: ollama pull nomic-embed-text
|
| 22 |
+
echo ollama pull llama3
|
| 23 |
+
)
|
| 24 |
+
echo.
|
| 25 |
+
echo [3/3] Starting Manager Intelligence Agent...
|
| 26 |
+
echo Open browser at: http://localhost:7860
|
| 27 |
+
echo.
|
| 28 |
+
python app.py
|
| 29 |
+
pause
|
app.py
ADDED
|
@@ -0,0 +1,1005 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Manager Intelligence Agent
|
| 3 |
+
100% Offline - Ollama - FAISS - Windows
|
| 4 |
+
"""
|
| 5 |
+
import os, re, json, shutil, pickle, hashlib, datetime, subprocess
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import numpy as np
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import requests
|
| 10 |
+
|
| 11 |
+
# ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
WATCH_FOLDERS = [r"D:\\"]
|
| 13 |
+
OLLAMA_URL = "http://localhost:11434"
|
| 14 |
+
CHAT_MODEL = "llama3"
|
| 15 |
+
EMBED_MODEL = "nomic-embed-text"
|
| 16 |
+
INDEX_DIR = os.path.join(os.path.expanduser("~"), "manager_agent_index")
|
| 17 |
+
TASKS_FILE = os.path.join(INDEX_DIR, "_tasks.json")
|
| 18 |
+
EVENTS_FILE = os.path.join(INDEX_DIR, "_events.json")
|
| 19 |
+
os.makedirs(INDEX_DIR, exist_ok=True)
|
| 20 |
+
SUPPORTED = {".pdf",".docx",".doc",".xlsx",".xls",".csv",".txt",".eml",".msg",".rtf",".pptx",".ppt"}
|
| 21 |
+
MAX_MB = 50
|
| 22 |
+
ICONS = {".pdf":"π",".docx":"π",".doc":"π",".xlsx":"π",".xls":"π",
|
| 23 |
+
".csv":"π",".pptx":"π",".ppt":"π",".txt":"π",".eml":"π§",".msg":"π§"}
|
| 24 |
+
|
| 25 |
+
# ββ OLLAMA βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
def ollama_ok():
|
| 27 |
+
try: return requests.get(f"{OLLAMA_URL}/api/tags", timeout=3).status_code == 200
|
| 28 |
+
except: return False
|
| 29 |
+
|
| 30 |
+
def ollama_models():
|
| 31 |
+
try: return [m["name"] for m in requests.get(f"{OLLAMA_URL}/api/tags", timeout=5).json().get("models",[])]
|
| 32 |
+
except: return []
|
| 33 |
+
|
| 34 |
+
def do_embed(texts):
|
| 35 |
+
out = []
|
| 36 |
+
for t in texts:
|
| 37 |
+
r = requests.post(f"{OLLAMA_URL}/api/embeddings",
|
| 38 |
+
json={"model": EMBED_MODEL, "prompt": t}, timeout=60)
|
| 39 |
+
out.append(r.json()["embedding"])
|
| 40 |
+
return out
|
| 41 |
+
|
| 42 |
+
def do_generate(prompt, model):
|
| 43 |
+
r = requests.post(f"{OLLAMA_URL}/api/generate",
|
| 44 |
+
json={"model": model, "prompt": prompt, "stream": False}, timeout=300)
|
| 45 |
+
txt = r.json().get("response", "")
|
| 46 |
+
return re.sub(r"<think>.*?</think>", "", txt, flags=re.DOTALL).strip()
|
| 47 |
+
|
| 48 |
+
def do_chat_llm(prompt, context, history, model):
|
| 49 |
+
sys_msg = ("You are an executive assistant AI with access to the manager's documents. "
|
| 50 |
+
"Answer precisely, cite document names, use bullet points, flag deadlines.")
|
| 51 |
+
msgs = [{"role":"system","content":sys_msg}]
|
| 52 |
+
for u, b in history[-4:]:
|
| 53 |
+
msgs += [{"role":"user","content":u}, {"role":"assistant","content":b}]
|
| 54 |
+
msgs.append({"role":"user","content":f"{prompt}\n\nContext:\n{context}"})
|
| 55 |
+
r = requests.post(f"{OLLAMA_URL}/api/chat",
|
| 56 |
+
json={"model": model, "messages": msgs, "stream": False}, timeout=300)
|
| 57 |
+
txt = r.json().get("message", {}).get("content", "")
|
| 58 |
+
return re.sub(r"<think>.*?</think>", "", txt, flags=re.DOTALL).strip() or "No response."
|
| 59 |
+
|
| 60 |
+
# ββ TEXT EXTRACTION ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 61 |
+
def extract(fp):
|
| 62 |
+
ext = Path(fp).suffix.lower()
|
| 63 |
+
try:
|
| 64 |
+
if ext == ".pdf":
|
| 65 |
+
import pdfplumber
|
| 66 |
+
with pdfplumber.open(fp) as pdf:
|
| 67 |
+
return "\n".join(p.extract_text() or "" for p in pdf.pages)
|
| 68 |
+
if ext in (".docx", ".doc"):
|
| 69 |
+
from docx import Document
|
| 70 |
+
doc = Document(fp)
|
| 71 |
+
parts = [p.text for p in doc.paragraphs if p.text.strip()]
|
| 72 |
+
for t in doc.tables:
|
| 73 |
+
for row in t.rows:
|
| 74 |
+
parts.append(" | ".join(c.text.strip() for c in row.cells if c.text.strip()))
|
| 75 |
+
return "\n".join(parts)
|
| 76 |
+
if ext in (".xlsx", ".xls"):
|
| 77 |
+
import pandas as pd
|
| 78 |
+
xl = pd.ExcelFile(fp)
|
| 79 |
+
return "\n\n".join(f"[{s}]\n{xl.parse(s).head(200).to_string(index=False)}" for s in xl.sheet_names)
|
| 80 |
+
if ext == ".csv":
|
| 81 |
+
import pandas as pd
|
| 82 |
+
return pd.read_csv(fp, encoding="utf-8", errors="ignore").to_string(index=False)
|
| 83 |
+
if ext in (".pptx", ".ppt"):
|
| 84 |
+
from pptx import Presentation
|
| 85 |
+
prs = Presentation(fp)
|
| 86 |
+
return "\n".join(" ".join(s.text for s in sl.shapes if hasattr(s,"text")) for sl in prs.slides)
|
| 87 |
+
return open(fp, "r", encoding="utf-8", errors="ignore").read()
|
| 88 |
+
except:
|
| 89 |
+
return ""
|
| 90 |
+
|
| 91 |
+
# ββ INDEXING βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 92 |
+
def fhash(fp):
|
| 93 |
+
return hashlib.md5(f"{fp}{os.path.getmtime(fp)}".encode()).hexdigest()[:12]
|
| 94 |
+
|
| 95 |
+
def is_indexed(fp):
|
| 96 |
+
return os.path.exists(f"{INDEX_DIR}/{fhash(fp)}.faiss")
|
| 97 |
+
|
| 98 |
+
def make_chunks(text, fname, size=350, overlap=70):
|
| 99 |
+
words = re.sub(r'\s+', ' ', text).strip().split()
|
| 100 |
+
chunks = []
|
| 101 |
+
for s in range(0, len(words), size - overlap):
|
| 102 |
+
e = min(s + size, len(words))
|
| 103 |
+
c = " ".join(words[s:e])
|
| 104 |
+
if len(c) > 50:
|
| 105 |
+
chunks.append({"text": c, "source": fname, "preview": c[:200]})
|
| 106 |
+
if e == len(words): break
|
| 107 |
+
return chunks
|
| 108 |
+
|
| 109 |
+
def index_file(fp):
|
| 110 |
+
import faiss
|
| 111 |
+
fname = Path(fp).name
|
| 112 |
+
if os.path.getsize(fp) / (1024*1024) > MAX_MB:
|
| 113 |
+
return False, f">{MAX_MB}MB skipped"
|
| 114 |
+
text = extract(fp)
|
| 115 |
+
if not text or len(text.strip()) < 30:
|
| 116 |
+
return False, "No text extracted"
|
| 117 |
+
chunks = make_chunks(text, fname)
|
| 118 |
+
if not chunks:
|
| 119 |
+
return False, "No chunks"
|
| 120 |
+
try:
|
| 121 |
+
vecs = do_embed([c["text"] for c in chunks])
|
| 122 |
+
except Exception as e:
|
| 123 |
+
return False, f"Embed failed: {e}"
|
| 124 |
+
dim = len(vecs[0])
|
| 125 |
+
idx = faiss.IndexFlatL2(dim)
|
| 126 |
+
idx.add(np.array(vecs, dtype=np.float32))
|
| 127 |
+
fh = fhash(fp)
|
| 128 |
+
faiss.write_index(idx, f"{INDEX_DIR}/{fh}.faiss")
|
| 129 |
+
meta = {
|
| 130 |
+
"filename": fname, "filepath": str(fp),
|
| 131 |
+
"ftype": Path(fp).suffix.upper().strip("."),
|
| 132 |
+
"words": len(text.split()),
|
| 133 |
+
"mb": round(os.path.getsize(fp) / (1024*1024), 2),
|
| 134 |
+
"date": datetime.datetime.fromtimestamp(os.path.getmtime(fp)).strftime("%Y-%m-%d")
|
| 135 |
+
}
|
| 136 |
+
with open(f"{INDEX_DIR}/{fh}.pkl", "wb") as f:
|
| 137 |
+
pickle.dump({"chunks": chunks, "meta": meta}, f)
|
| 138 |
+
return True, f"{len(chunks)} chunks"
|
| 139 |
+
|
| 140 |
+
def scan_folder(folder, prog=None):
|
| 141 |
+
p = Path(folder)
|
| 142 |
+
if not p.exists():
|
| 143 |
+
return 0, 0, [f"β Not found: {folder}"]
|
| 144 |
+
all_f = [f for f in p.rglob("*") if f.is_file() and f.suffix.lower() in SUPPORTED]
|
| 145 |
+
new_f = [f for f in all_f if not is_indexed(f)]
|
| 146 |
+
log = [f"π {len(all_f)} total Β· {len(new_f)} new"]
|
| 147 |
+
ok = fail = 0
|
| 148 |
+
for i, fp in enumerate(new_f):
|
| 149 |
+
if prog: prog(i, len(new_f), fp.name)
|
| 150 |
+
good, msg = index_file(fp)
|
| 151 |
+
if good: ok += 1; log.append(f"β
{fp.name} β {msg}")
|
| 152 |
+
else: fail += 1; log.append(f"β οΈ {fp.name} β {msg}")
|
| 153 |
+
log.append(f"\nβ
Done: {ok} indexed Β· {fail} failed")
|
| 154 |
+
return ok, fail, log
|
| 155 |
+
|
| 156 |
+
# ββ SEARCH βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 157 |
+
def load_all():
|
| 158 |
+
import faiss
|
| 159 |
+
chunks, merged = [], None
|
| 160 |
+
for ff in os.listdir(INDEX_DIR):
|
| 161 |
+
if not ff.endswith(".faiss"): continue
|
| 162 |
+
pkl = f"{INDEX_DIR}/{ff[:-6]}.pkl"
|
| 163 |
+
if not os.path.exists(pkl): continue
|
| 164 |
+
idx = faiss.read_index(f"{INDEX_DIR}/{ff}")
|
| 165 |
+
with open(pkl, "rb") as f: data = pickle.load(f)
|
| 166 |
+
if idx.ntotal == 0: continue
|
| 167 |
+
vecs = idx.reconstruct_n(0, idx.ntotal)
|
| 168 |
+
if merged is None: merged = faiss.IndexFlatL2(vecs.shape[1])
|
| 169 |
+
for c in data["chunks"]:
|
| 170 |
+
c = c.copy(); c["meta"] = data["meta"]; chunks.append(c)
|
| 171 |
+
merged.add(vecs)
|
| 172 |
+
return merged, chunks
|
| 173 |
+
|
| 174 |
+
def run_search(query):
|
| 175 |
+
"""Returns (html, dropdown_choices, dropdown_update)"""
|
| 176 |
+
EMPTY_DD = gr.Dropdown(choices=[], value=None, label="Select result to open")
|
| 177 |
+
|
| 178 |
+
if not query.strip():
|
| 179 |
+
return "<p style='color:#6b7280;padding:20px;text-align:center'>Enter a search query above.</p>", [], EMPTY_DD
|
| 180 |
+
|
| 181 |
+
if not ollama_ok():
|
| 182 |
+
return "<p style='color:#dc2626;padding:20px'>β Ollama not running. Start Ollama first.</p>", [], EMPTY_DD
|
| 183 |
+
|
| 184 |
+
idx, chunks = load_all()
|
| 185 |
+
if idx is None:
|
| 186 |
+
return "<p style='color:#dc2626;padding:20px'>β No files indexed yet. Go to Documents tab first.</p>", [], EMPTY_DD
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
qv = np.array([do_embed([query])[0]], dtype=np.float32)
|
| 190 |
+
except Exception as e:
|
| 191 |
+
return f"<p style='color:#dc2626'>β Embedding error: {e}</p>", [], EMPTY_DD
|
| 192 |
+
|
| 193 |
+
stop = {"the","a","an","is","in","on","at","to","for","of","and","or","it","was","are","with","this","that"}
|
| 194 |
+
kws = {w.lower() for w in re.findall(r'\w+', query) if len(w) > 2} - stop
|
| 195 |
+
|
| 196 |
+
k = min(20, idx.ntotal)
|
| 197 |
+
dists, idxs = idx.search(qv, k)
|
| 198 |
+
|
| 199 |
+
seen = {}
|
| 200 |
+
for dist, i in zip(dists[0], idxs[0]):
|
| 201 |
+
if i < 0 or i >= len(chunks): continue
|
| 202 |
+
c = chunks[i]
|
| 203 |
+
sc = float(1 / (1 + dist))
|
| 204 |
+
kh = sum(1 for kw in kws if kw in c["text"].lower())
|
| 205 |
+
final = sc + kh * 0.1
|
| 206 |
+
fn = c["source"]
|
| 207 |
+
if fn not in seen or seen[fn]["score"] < final:
|
| 208 |
+
seen[fn] = {"chunk": c, "score": final, "kw": kh}
|
| 209 |
+
|
| 210 |
+
results = sorted(seen.items(), key=lambda x: -x[1]["score"])[:8]
|
| 211 |
+
if not results:
|
| 212 |
+
return "<p style='color:#6b7280;padding:20px;text-align:center'>No matching documents found.</p>", [], EMPTY_DD
|
| 213 |
+
|
| 214 |
+
html = f"<p style='color:#374151;margin-bottom:12px;font-size:.85rem'>β
Found <strong>{len(results)} documents</strong> for: <em>{query}</em></p>"
|
| 215 |
+
choices = []
|
| 216 |
+
|
| 217 |
+
for fname, v in results:
|
| 218 |
+
m = v["chunk"]["meta"]
|
| 219 |
+
sc2 = min(int(v["score"] * 100), 99)
|
| 220 |
+
icon = ICONS.get("." + m.get("ftype","").lower(), "π")
|
| 221 |
+
fp = m.get("filepath", "")
|
| 222 |
+
prev = v["chunk"]["preview"]
|
| 223 |
+
for kw in kws:
|
| 224 |
+
prev = re.sub(f"({re.escape(kw)})", r"<mark style='background:#fef08a;color:#713f12;border-radius:2px;padding:0 2px'>\1</mark>", prev, flags=re.IGNORECASE)
|
| 225 |
+
|
| 226 |
+
html += f"""
|
| 227 |
+
<div style="background:#fff;border:1.5px solid #d1d5db;border-left:5px solid #1d4ed8;
|
| 228 |
+
border-radius:10px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.07)">
|
| 229 |
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px;margin-bottom:8px">
|
| 230 |
+
<div style="display:flex;gap:10px;align-items:center">
|
| 231 |
+
<span style="font-size:1.6rem">{icon}</span>
|
| 232 |
+
<div>
|
| 233 |
+
<div style="font-size:.92rem;font-weight:700;color:#111827">{fname}</div>
|
| 234 |
+
<div style="font-size:.68rem;color:#9ca3af;font-family:monospace;margin-top:2px;word-break:break-all">{fp}</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
| 238 |
+
<span style="background:#eff6ff;border:1px solid #bfdbfe;color:#1d4ed8;padding:3px 10px;border-radius:50px;font-size:.72rem;font-weight:700">Match {sc2}%</span>
|
| 239 |
+
{f'<span style="background:#fffbeb;border:1px solid #fde68a;color:#d97706;padding:3px 10px;border-radius:50px;font-size:.72rem;font-weight:700">π {v["kw"]} hits</span>' if v["kw"] else ""}
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
<div style="font-size:.72rem;color:#9ca3af;margin-bottom:8px">
|
| 243 |
+
π
{m.get('date','')} Β· {m.get('words',0):,} words Β· {m.get('ftype','')} Β· {m.get('mb',0)} MB
|
| 244 |
+
</div>
|
| 245 |
+
<div style="font-size:.82rem;color:#374151;line-height:1.65;border-top:1px solid #f3f4f6;padding-top:8px">{prev}β¦</div>
|
| 246 |
+
</div>"""
|
| 247 |
+
label = f"{fname} [{fp}]"
|
| 248 |
+
choices.append(label)
|
| 249 |
+
|
| 250 |
+
dd = gr.Dropdown(choices=choices, value=choices[0] if choices else None,
|
| 251 |
+
label="π Select a file then click Open File or Show in Folder")
|
| 252 |
+
return html, choices, dd
|
| 253 |
+
|
| 254 |
+
def open_file(choice):
|
| 255 |
+
if not choice: return "β οΈ Select a file from the dropdown first."
|
| 256 |
+
# Extract path from "filename [filepath]"
|
| 257 |
+
m = re.search(r'\[(.+)\]$', choice)
|
| 258 |
+
fp = m.group(1).strip() if m else ""
|
| 259 |
+
if not fp: return "β οΈ Could not extract file path."
|
| 260 |
+
if not os.path.exists(fp): return f"β File not found on disk:\n{fp}"
|
| 261 |
+
try:
|
| 262 |
+
os.startfile(fp)
|
| 263 |
+
return f"β
Opened: {os.path.basename(fp)}"
|
| 264 |
+
except Exception as e:
|
| 265 |
+
return f"β Error opening file: {e}"
|
| 266 |
+
|
| 267 |
+
def show_in_folder(choice):
|
| 268 |
+
if not choice: return "β οΈ Select a file from the dropdown first."
|
| 269 |
+
m = re.search(r'\[(.+)\]$', choice)
|
| 270 |
+
fp = m.group(1).strip() if m else ""
|
| 271 |
+
if not fp: return "β οΈ Could not extract file path."
|
| 272 |
+
if not os.path.exists(fp): return f"β File not found on disk:\n{fp}"
|
| 273 |
+
try:
|
| 274 |
+
subprocess.Popen(['explorer', '/select,', fp])
|
| 275 |
+
return f"β
Revealed in Explorer: {os.path.basename(fp)}"
|
| 276 |
+
except Exception as e:
|
| 277 |
+
return f"β Error: {e}"
|
| 278 |
+
|
| 279 |
+
# ββ DOCUMENT HELPERS βββββββββββββββββββββββββββββββββββββββββββββ
|
| 280 |
+
def get_text(fname):
|
| 281 |
+
for ff in os.listdir(INDEX_DIR):
|
| 282 |
+
if not ff.endswith(".pkl"): continue
|
| 283 |
+
with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
|
| 284 |
+
if data["meta"]["filename"] == fname:
|
| 285 |
+
return "\n\n".join(c["text"] for c in data["chunks"])
|
| 286 |
+
return ""
|
| 287 |
+
|
| 288 |
+
def get_fp(fname):
|
| 289 |
+
for ff in os.listdir(INDEX_DIR):
|
| 290 |
+
if not ff.endswith(".pkl"): continue
|
| 291 |
+
with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
|
| 292 |
+
if data["meta"]["filename"] == fname:
|
| 293 |
+
return data["meta"].get("filepath", "")
|
| 294 |
+
return ""
|
| 295 |
+
|
| 296 |
+
def all_meta():
|
| 297 |
+
docs = []
|
| 298 |
+
for ff in os.listdir(INDEX_DIR):
|
| 299 |
+
if not ff.endswith(".pkl"): continue
|
| 300 |
+
with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
|
| 301 |
+
docs.append(data["meta"])
|
| 302 |
+
return sorted(docs, key=lambda x: x.get("date",""), reverse=True)
|
| 303 |
+
|
| 304 |
+
def all_names(): return [d["filename"] for d in all_meta()]
|
| 305 |
+
|
| 306 |
+
def lib_stats():
|
| 307 |
+
docs = all_meta()
|
| 308 |
+
if not docs: return "*No files indexed yet.*"
|
| 309 |
+
tw = sum(d.get("words",0) for d in docs)
|
| 310 |
+
lines = [f"**π {len(docs)} files Β· {tw:,} words**\n"]
|
| 311 |
+
for d in docs[:40]:
|
| 312 |
+
icon = ICONS.get("." + d.get("ftype","").lower(), "π")
|
| 313 |
+
lines.append(f"{icon} **{d['filename']}** Β· {d.get('words',0):,}w Β· {d.get('date','')} Β· {d.get('ftype','')}")
|
| 314 |
+
if len(docs) > 40: lines.append(f"*...and {len(docs)-40} more*")
|
| 315 |
+
return "\n".join(lines)
|
| 316 |
+
|
| 317 |
+
# ββ TASKS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 318 |
+
def load_tasks():
|
| 319 |
+
try:
|
| 320 |
+
if os.path.exists(TASKS_FILE):
|
| 321 |
+
with open(TASKS_FILE) as f: return json.load(f)
|
| 322 |
+
except: pass
|
| 323 |
+
return []
|
| 324 |
+
|
| 325 |
+
def save_tasks(t):
|
| 326 |
+
with open(TASKS_FILE, "w") as f: json.dump(t, f, indent=2)
|
| 327 |
+
|
| 328 |
+
def tasks_html():
|
| 329 |
+
tasks = load_tasks()
|
| 330 |
+
today = datetime.date.today().isoformat()
|
| 331 |
+
if not tasks:
|
| 332 |
+
return "<p style='padding:20px;text-align:center;color:#6b7280;background:#f9fafb;border:2px dashed #d1d5db;border-radius:8px'>No tasks yet.</p>"
|
| 333 |
+
rows = ""
|
| 334 |
+
for i, t in enumerate(tasks):
|
| 335 |
+
done = t.get("done", False)
|
| 336 |
+
due = t.get("due", "")
|
| 337 |
+
ov = due and due < today and not done
|
| 338 |
+
bg = "#fef2f2" if ov else ("#f9fafb" if done else "#fff")
|
| 339 |
+
bl = "#dc2626" if ov else ("#d1d5db" if done else "#1d4ed8")
|
| 340 |
+
op = "0.55" if done else "1"
|
| 341 |
+
pri = t.get("priority","medium")
|
| 342 |
+
pc = {"high":"#dc2626","medium":"#d97706","low":"#15803d"}.get(pri,"#6b7280")
|
| 343 |
+
pb = {"high":"#fef2f2","medium":"#fffbeb","low":"#f0fdf4"}.get(pri,"#f9fafb")
|
| 344 |
+
rows += f"""<div style="display:flex;align-items:center;gap:10px;background:{bg};
|
| 345 |
+
border:1px solid #e5e7eb;border-left:4px solid {bl};border-radius:8px;padding:11px 14px;
|
| 346 |
+
opacity:{op};margin-bottom:7px">
|
| 347 |
+
<span style="font-family:monospace;font-size:.68rem;color:#9ca3af;background:#f3f4f6;
|
| 348 |
+
border:1px solid #e5e7eb;border-radius:4px;padding:1px 6px;flex-shrink:0">#{i}</span>
|
| 349 |
+
<div style="flex:1">
|
| 350 |
+
<div style="font-size:.85rem;font-weight:500;color:#111827;{'text-decoration:line-through;color:#9ca3af' if done else ''}">{t['text']}</div>
|
| 351 |
+
{f'<div style="font-size:.70rem;color:{"#dc2626" if ov else "#9ca3af"};margin-top:2px">π
{due}{" β οΈ OVERDUE" if ov else ""}</div>' if due else ''}
|
| 352 |
+
</div>
|
| 353 |
+
<span style="font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:50px;
|
| 354 |
+
background:{pb};color:{pc};border:1px solid {pc}40">{pri.upper()}</span>
|
| 355 |
+
{"<span style='font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:50px;background:#f0fdf4;color:#15803d;border:1px solid #86efac'>DONE</span>" if done else ""}
|
| 356 |
+
</div>"""
|
| 357 |
+
return rows + "<p style='font-size:.70rem;color:#9ca3af;text-align:center;margin-top:4px;font-style:italic'>Use task # to toggle done or delete</p>"
|
| 358 |
+
|
| 359 |
+
# ββ EVENTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 360 |
+
def load_events():
|
| 361 |
+
try:
|
| 362 |
+
if os.path.exists(EVENTS_FILE):
|
| 363 |
+
with open(EVENTS_FILE) as f: return json.load(f)
|
| 364 |
+
except: pass
|
| 365 |
+
return []
|
| 366 |
+
|
| 367 |
+
def save_events(e):
|
| 368 |
+
with open(EVENTS_FILE, "w") as f: json.dump(e, f, indent=2)
|
| 369 |
+
|
| 370 |
+
def events_html():
|
| 371 |
+
evs = load_events()
|
| 372 |
+
today = datetime.date.today().isoformat()
|
| 373 |
+
up = sorted([e for e in evs if e.get("date","") >= today], key=lambda x: x["date"])
|
| 374 |
+
past = sorted([e for e in evs if e.get("date","") < today], key=lambda x: x["date"], reverse=True)[:3]
|
| 375 |
+
if not up and not past:
|
| 376 |
+
return "<p style='padding:20px;text-align:center;color:#6b7280;background:#f9fafb;border:2px dashed #d1d5db;border-radius:8px'>No events yet.</p>"
|
| 377 |
+
def row(e, old=False):
|
| 378 |
+
try: day=datetime.datetime.strptime(e["date"],"%Y-%m-%d").strftime("%d"); mon=datetime.datetime.strptime(e["date"],"%Y-%m-%d").strftime("%b %Y")
|
| 379 |
+
except: day=e.get("date",""); mon=""
|
| 380 |
+
return f"""<div style="display:flex;align-items:center;gap:12px;background:{'#f9fafb' if old else '#fff'};
|
| 381 |
+
border:1px solid #e5e7eb;border-radius:8px;padding:11px 14px;margin-bottom:7px;opacity:{'0.45' if old else '1'}">
|
| 382 |
+
<div style="text-align:center;background:#eff6ff;border-radius:6px;padding:6px 10px;min-width:50px;flex-shrink:0">
|
| 383 |
+
<div style="font-size:1.3rem;font-weight:800;color:#1d4ed8;line-height:1">{day}</div>
|
| 384 |
+
<div style="font-size:.58rem;color:#3b82f6;text-transform:uppercase">{mon}</div>
|
| 385 |
+
</div>
|
| 386 |
+
<div>
|
| 387 |
+
<div style="font-size:.85rem;font-weight:600;color:#111827">{e['title']}</div>
|
| 388 |
+
{f'<div style="font-size:.70rem;color:#6b7280;margin-top:2px">π {e["time"]}</div>' if e.get("time") else ''}
|
| 389 |
+
{f'<div style="font-size:.70rem;color:#9ca3af;font-style:italic">{e["note"]}</div>' if e.get("note") else ''}
|
| 390 |
+
</div>
|
| 391 |
+
</div>"""
|
| 392 |
+
html = ""
|
| 393 |
+
if up:
|
| 394 |
+
html += "<p style='font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px'>π
Upcoming</p>"
|
| 395 |
+
html += "".join(row(e) for e in up[:10])
|
| 396 |
+
if past:
|
| 397 |
+
html += "<p style='font-size:.72rem;font-weight:700;color:#9ca3af;text-transform:uppercase;letter-spacing:.08em;margin:10px 0 6px'>Past</p>"
|
| 398 |
+
html += "".join(row(e, True) for e in past)
|
| 399 |
+
return html
|
| 400 |
+
|
| 401 |
+
# ββ DASHBOARD ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 402 |
+
def dashboard_html():
|
| 403 |
+
tasks = load_tasks(); today = datetime.date.today().isoformat()
|
| 404 |
+
today_str = datetime.date.today().strftime("%A, %B %d, %Y")
|
| 405 |
+
hr = datetime.datetime.now().hour
|
| 406 |
+
greet = "Good morning" if hr < 12 else "Good afternoon" if hr < 17 else "Good evening"
|
| 407 |
+
pending = [t for t in tasks if not t.get("done")]
|
| 408 |
+
overdue = [t for t in pending if t.get("due","") and t["due"] < today]
|
| 409 |
+
hi = [t for t in pending if t.get("priority") == "high"]
|
| 410 |
+
evs = load_events()
|
| 411 |
+
up = sorted([e for e in evs if e.get("date","") >= today], key=lambda x: x["date"])[:5]
|
| 412 |
+
docs = all_meta(); recent = docs[:6]
|
| 413 |
+
|
| 414 |
+
def stat(n, lbl, bg, tc, bc):
|
| 415 |
+
return f"""<div style="background:{bg};border:1.5px solid {bc};border-radius:10px;padding:16px 18px;box-shadow:0 1px 4px rgba(0,0,0,.06)">
|
| 416 |
+
<div style="font-size:1.9rem;font-weight:800;color:{tc};line-height:1">{n}</div>
|
| 417 |
+
<div style="font-size:.70rem;color:{tc};font-weight:600;text-transform:uppercase;letter-spacing:.07em;margin-top:5px;opacity:.8">{lbl}</div>
|
| 418 |
+
</div>"""
|
| 419 |
+
|
| 420 |
+
stats = f"""<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:20px">
|
| 421 |
+
{stat(len(docs),"Indexed Docs","#eff6ff","#1d4ed8","#bfdbfe")}
|
| 422 |
+
{stat(len(pending),"Pending Tasks","#fffbeb","#d97706","#fde68a")}
|
| 423 |
+
{stat(len(overdue),"Overdue","#fef2f2" if overdue else "#f0fdf4","#dc2626" if overdue else "#15803d","#fecaca" if overdue else "#bbf7d0")}
|
| 424 |
+
{stat(len(hi),"High Priority","#fef2f2" if hi else "#f0fdf4","#dc2626" if hi else "#15803d","#fecaca" if hi else "#bbf7d0")}
|
| 425 |
+
{stat(len(up),"Upcoming Events","#f5f3ff","#7c3aed","#ddd6fe")}
|
| 426 |
+
</div>"""
|
| 427 |
+
|
| 428 |
+
def card(title, rows_html, empty_msg):
|
| 429 |
+
return f"""<div style="background:#fff;border:1.5px solid #e5e7eb;border-radius:10px;padding:16px 18px;box-shadow:0 1px 4px rgba(0,0,0,.06)">
|
| 430 |
+
<div style="font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px;padding-bottom:8px;border-bottom:2px solid #fef3c7">{title}</div>
|
| 431 |
+
{rows_html or f'<p style="color:#9ca3af;font-size:.80rem;text-align:center;padding:12px 0">{empty_msg}</p>'}
|
| 432 |
+
</div>"""
|
| 433 |
+
|
| 434 |
+
def drow(icon, text, meta):
|
| 435 |
+
return f"""<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f9fafb;font-size:.82rem">
|
| 436 |
+
<span>{icon}</span><span style="flex:1;color:#374151;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{text}</span>
|
| 437 |
+
<span style="font-size:.68rem;color:#9ca3af">{meta}</span>
|
| 438 |
+
</div>"""
|
| 439 |
+
|
| 440 |
+
task_rows = "".join(drow("β¬", t["text"][:45], t.get("due","")) for t in pending[:5])
|
| 441 |
+
ev_rows = "".join(drow("π
", e["title"][:45], e["date"]) for e in up)
|
| 442 |
+
doc_rows = "".join(drow(ICONS.get("."+d.get("ftype","").lower(),"π"), d["filename"][:45], d.get("date","")) for d in recent)
|
| 443 |
+
|
| 444 |
+
return f"""<div style="padding:4px 0">
|
| 445 |
+
<div style="font-size:1.5rem;font-weight:800;color:#111827;letter-spacing:-.02em">{greet}, Manager</div>
|
| 446 |
+
<div style="font-size:.80rem;color:#6b7280;margin-bottom:20px">{today_str}</div>
|
| 447 |
+
{stats}
|
| 448 |
+
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px">
|
| 449 |
+
{card("π Active Tasks", task_rows, "All tasks complete! π")}
|
| 450 |
+
{card("ποΈ Upcoming Events", ev_rows, "No upcoming events")}
|
| 451 |
+
{card("π Recent Documents", doc_rows, "No documents indexed yet")}
|
| 452 |
+
</div>
|
| 453 |
+
</div>"""
|
| 454 |
+
|
| 455 |
+
# ββ CHAT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 456 |
+
def do_chat(message, history, model, focus):
|
| 457 |
+
if not message.strip(): return history, ""
|
| 458 |
+
if not ollama_ok(): return history + [[message, "β Ollama not running."]], ""
|
| 459 |
+
ctx = []
|
| 460 |
+
if focus and focus not in ("", "β All Documents β"):
|
| 461 |
+
t = get_text(focus)
|
| 462 |
+
if t: ctx.append(f"[{focus}]\n{t[:3000]}")
|
| 463 |
+
else:
|
| 464 |
+
idx, chunks = load_all()
|
| 465 |
+
if idx is not None:
|
| 466 |
+
try:
|
| 467 |
+
qv = np.array([do_embed([message])[0]], dtype=np.float32)
|
| 468 |
+
k = min(10, idx.ntotal)
|
| 469 |
+
dists, idxs = idx.search(qv, k)
|
| 470 |
+
seen = set()
|
| 471 |
+
for dist, i in zip(dists[0], idxs[0]):
|
| 472 |
+
if i < 0 or i >= len(chunks): continue
|
| 473 |
+
fn = chunks[i]["source"]
|
| 474 |
+
if fn not in seen:
|
| 475 |
+
seen.add(fn)
|
| 476 |
+
t = get_text(fn)
|
| 477 |
+
if t: ctx.append(f"[{fn}]\n{t[:800]}")
|
| 478 |
+
if len(seen) >= 5: break
|
| 479 |
+
except: pass
|
| 480 |
+
context = "\n\n---\n\n".join(ctx) if ctx else "No documents indexed yet."
|
| 481 |
+
try:
|
| 482 |
+
ans = do_chat_llm(message, context, history[-4:], model)
|
| 483 |
+
except Exception as e:
|
| 484 |
+
ans = f"β Ollama error: {e}"
|
| 485 |
+
return history + [[message, ans]], ""
|
| 486 |
+
|
| 487 |
+
def do_analyze(filename, model):
|
| 488 |
+
if not filename: return [["", "β οΈ Select a document first."]], []
|
| 489 |
+
if not ollama_ok(): return [["", "β Ollama not running."]], []
|
| 490 |
+
text = get_text(filename)
|
| 491 |
+
if not text: return [["", f"β '{filename}' not in index."]], []
|
| 492 |
+
prompt = f"""Analyze this document as an executive assistant.
|
| 493 |
+
|
| 494 |
+
# Analysis: {filename}
|
| 495 |
+
|
| 496 |
+
## Executive Summary
|
| 497 |
+
[2-3 concise sentences a busy executive needs to know]
|
| 498 |
+
|
| 499 |
+
## Key People
|
| 500 |
+
[All names and roles mentioned]
|
| 501 |
+
|
| 502 |
+
## Important Dates
|
| 503 |
+
[Every date with context]
|
| 504 |
+
|
| 505 |
+
## Financial Data
|
| 506 |
+
[All numbers, amounts, percentages]
|
| 507 |
+
|
| 508 |
+
## Decisions & Action Items
|
| 509 |
+
[What was decided and what must happen]
|
| 510 |
+
|
| 511 |
+
## Risks & Flags
|
| 512 |
+
[Things the manager must watch]
|
| 513 |
+
|
| 514 |
+
Document:
|
| 515 |
+
{text[:4000]}"""
|
| 516 |
+
try:
|
| 517 |
+
res = do_generate(prompt, model)
|
| 518 |
+
return [[f"π {filename}", res]], []
|
| 519 |
+
except Exception as e:
|
| 520 |
+
return [[f"π {filename}", f"β Error: {e}"]], []
|
| 521 |
+
|
| 522 |
+
# ββ EMAIL ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 523 |
+
def do_email(instructions, doc, tone, model):
|
| 524 |
+
if not instructions.strip():
|
| 525 |
+
return "β οΈ Please describe what the email should say."
|
| 526 |
+
if not ollama_ok():
|
| 527 |
+
return "β Ollama not running. Start Ollama first then try again."
|
| 528 |
+
ctx = ""
|
| 529 |
+
if doc and doc not in ("", "β None β"):
|
| 530 |
+
t = get_text(doc)
|
| 531 |
+
if t: ctx = f"\n\nDocument context ({doc}):\n{t[:2000]}"
|
| 532 |
+
tones = {
|
| 533 |
+
"Formal & Executive": "formal, authoritative, executive-level",
|
| 534 |
+
"Professional & Warm": "professional but warm and approachable",
|
| 535 |
+
"Concise & Direct": "very concise, direct, no filler words",
|
| 536 |
+
"Diplomatic": "diplomatic, careful, politically nuanced"
|
| 537 |
+
}
|
| 538 |
+
tone_desc = tones.get(tone, "formal")
|
| 539 |
+
prompt = f"""Write a complete professional business email.
|
| 540 |
+
Tone: {tone_desc}
|
| 541 |
+
Instructions: {instructions}{ctx}
|
| 542 |
+
|
| 543 |
+
Write the full email in this EXACT format:
|
| 544 |
+
|
| 545 |
+
Subject: [write the subject line here]
|
| 546 |
+
|
| 547 |
+
Dear [Recipient name or title],
|
| 548 |
+
|
| 549 |
+
[Write the email body here with proper paragraphs]
|
| 550 |
+
|
| 551 |
+
Best regards,
|
| 552 |
+
[Manager Name]"""
|
| 553 |
+
try:
|
| 554 |
+
return do_generate(prompt, model)
|
| 555 |
+
except Exception as e:
|
| 556 |
+
return f"β Error generating email: {e}"
|
| 557 |
+
|
| 558 |
+
# ββ TASK HANDLERS ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 559 |
+
def add_task(txt, due, pri, note):
|
| 560 |
+
if not txt.strip(): return tasks_html(), "β οΈ Enter task text", "", "", "medium", ""
|
| 561 |
+
t = load_tasks()
|
| 562 |
+
t.append({"text":txt.strip(),"due":due.strip(),"priority":pri,"note":note,
|
| 563 |
+
"done":False,"created":datetime.date.today().isoformat()})
|
| 564 |
+
save_tasks(t)
|
| 565 |
+
return tasks_html(), "", "", "", "medium", ""
|
| 566 |
+
|
| 567 |
+
def toggle_task(idx):
|
| 568 |
+
t = load_tasks()
|
| 569 |
+
try:
|
| 570 |
+
i = int(idx.strip())
|
| 571 |
+
if 0 <= i < len(t): t[i]["done"] = not t[i]["done"]
|
| 572 |
+
save_tasks(t)
|
| 573 |
+
except: pass
|
| 574 |
+
return tasks_html(), ""
|
| 575 |
+
|
| 576 |
+
def delete_task(idx):
|
| 577 |
+
t = load_tasks()
|
| 578 |
+
try:
|
| 579 |
+
i = int(idx.strip())
|
| 580 |
+
if 0 <= i < len(t): t.pop(i)
|
| 581 |
+
save_tasks(t)
|
| 582 |
+
except: pass
|
| 583 |
+
return tasks_html(), ""
|
| 584 |
+
|
| 585 |
+
# ββ EVENT HANDLERS βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 586 |
+
def add_event(title, date, time, note):
|
| 587 |
+
if not title.strip() or not date.strip():
|
| 588 |
+
return events_html(), "β οΈ Title and date required", "", "", "", ""
|
| 589 |
+
e = load_events()
|
| 590 |
+
e.append({"title":title.strip(),"date":date.strip(),"time":time.strip(),"note":note.strip()})
|
| 591 |
+
save_events(e)
|
| 592 |
+
return events_html(), "", "", "", "", ""
|
| 593 |
+
|
| 594 |
+
def delete_event(idx):
|
| 595 |
+
e = load_events()
|
| 596 |
+
try:
|
| 597 |
+
i = int(idx.strip())
|
| 598 |
+
if 0 <= i < len(e): e.pop(i)
|
| 599 |
+
save_events(e)
|
| 600 |
+
except: pass
|
| 601 |
+
return events_html(), ""
|
| 602 |
+
|
| 603 |
+
# ββ INDEX HANDLERS βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 604 |
+
def do_scan(folders_text, progress=gr.Progress()):
|
| 605 |
+
if not ollama_ok(): return "β Ollama not running.", lib_stats()
|
| 606 |
+
folders = [f.strip() for f in folders_text.split("\n") if f.strip()] or WATCH_FOLDERS
|
| 607 |
+
log = []
|
| 608 |
+
for folder in folders:
|
| 609 |
+
log.append(f"\nπ Scanning: {folder}")
|
| 610 |
+
def prog(i, total, name): progress(i / max(total,1), desc=f"Indexing {name}")
|
| 611 |
+
_, _, fl = scan_folder(folder, prog)
|
| 612 |
+
log.extend(fl)
|
| 613 |
+
return "\n".join(log), lib_stats()
|
| 614 |
+
|
| 615 |
+
def do_upload(files, progress=gr.Progress()):
|
| 616 |
+
if not ollama_ok(): return "β Ollama not running.", lib_stats()
|
| 617 |
+
if not files: return "β οΈ No files selected.", lib_stats()
|
| 618 |
+
results = []
|
| 619 |
+
for i, f in enumerate(files):
|
| 620 |
+
progress(i / len(files), desc=f"Indexing {Path(f.name).name}")
|
| 621 |
+
good, msg = index_file(f.name)
|
| 622 |
+
results.append(f"{'β
' if good else 'β οΈ'} {Path(f.name).name} β {msg}")
|
| 623 |
+
return "\n".join(results), lib_stats()
|
| 624 |
+
|
| 625 |
+
def do_clear():
|
| 626 |
+
shutil.rmtree(INDEX_DIR, ignore_errors=True)
|
| 627 |
+
os.makedirs(INDEX_DIR, exist_ok=True)
|
| 628 |
+
return "ποΈ Index cleared.", lib_stats()
|
| 629 |
+
|
| 630 |
+
def do_load(fname):
|
| 631 |
+
if not fname: return "*Select a file.*", ""
|
| 632 |
+
text = get_text(fname)
|
| 633 |
+
fp = get_fp(fname)
|
| 634 |
+
if not text: return f"β '{fname}' not found.", ""
|
| 635 |
+
for ff in os.listdir(INDEX_DIR):
|
| 636 |
+
if not ff.endswith(".pkl"): continue
|
| 637 |
+
with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
|
| 638 |
+
if data["meta"]["filename"] == fname:
|
| 639 |
+
m = data["meta"]
|
| 640 |
+
return f"**{fname}** Β· {m.get('words',0):,} words Β· {m.get('mb',0)} MB Β· {m.get('date','')} Β· `{fp}`", text
|
| 641 |
+
return f"**{fname}**", text
|
| 642 |
+
|
| 643 |
+
def open_indexed(fname):
|
| 644 |
+
if not fname: return "οΏ½οΏ½οΈ Select a file first."
|
| 645 |
+
fp = get_fp(fname)
|
| 646 |
+
if not fp or not os.path.exists(fp): return f"β File not found on disk."
|
| 647 |
+
try: os.startfile(fp); return f"β
Opened: {fname}"
|
| 648 |
+
except Exception as e: return f"β {e}"
|
| 649 |
+
|
| 650 |
+
def locate_indexed(fname):
|
| 651 |
+
if not fname: return "β οΈ Select a file first."
|
| 652 |
+
fp = get_fp(fname)
|
| 653 |
+
if not fp or not os.path.exists(fp): return f"β File not found on disk."
|
| 654 |
+
try: subprocess.Popen(['explorer', '/select,', fp]); return f"β
Revealed in Explorer"
|
| 655 |
+
except Exception as e: return f"β {e}"
|
| 656 |
+
|
| 657 |
+
# ββ CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 658 |
+
CSS = """
|
| 659 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
| 660 |
+
body, .gradio-container { background:#f0f4f8!important; font-family:'Inter',sans-serif!important; color:#111827!important; }
|
| 661 |
+
.gradio-container { max-width:100%!important; padding:0!important; }
|
| 662 |
+
.gr-tab-nav { background:#fff!important; border-bottom:2px solid #e5e7eb!important; padding:0 24px!important; }
|
| 663 |
+
.gr-tab-nav button { font-family:'Inter',sans-serif!important; font-size:.82rem!important; font-weight:600!important;
|
| 664 |
+
color:#6b7280!important; background:transparent!important; border:none!important;
|
| 665 |
+
border-bottom:3px solid transparent!important; padding:13px 18px!important; margin-bottom:-2px!important; }
|
| 666 |
+
.gr-tab-nav button:hover { color:#1d4ed8!important; }
|
| 667 |
+
.gr-tab-nav button.selected { color:#1d4ed8!important; border-bottom-color:#1d4ed8!important; }
|
| 668 |
+
textarea, input[type=text] { background:#fff!important; border:1.5px solid #d1d5db!important;
|
| 669 |
+
border-radius:8px!important; color:#111827!important; font-family:'Inter',sans-serif!important; font-size:.86rem!important; }
|
| 670 |
+
textarea:focus, input:focus { border-color:#1d4ed8!important; outline:none!important; box-shadow:0 0 0 3px rgba(29,78,216,.1)!important; }
|
| 671 |
+
label span { color:#1d4ed8!important; font-size:.68rem!important; font-weight:700!important; text-transform:uppercase!important; letter-spacing:.08em!important; }
|
| 672 |
+
.gr-button { font-family:'Inter',sans-serif!important; font-weight:600!important; border-radius:8px!important; font-size:.83rem!important; }
|
| 673 |
+
.gr-button.primary { background:#1d4ed8!important; color:#fff!important; border:none!important; }
|
| 674 |
+
.gr-button.primary:hover { background:#1e40af!important; }
|
| 675 |
+
.gr-button.secondary { background:#fff!important; color:#374151!important; border:1.5px solid #d1d5db!important; }
|
| 676 |
+
.gr-button.secondary:hover { border-color:#1d4ed8!important; color:#1d4ed8!important; }
|
| 677 |
+
.gr-chatbot { background:#fff!important; border:1.5px solid #e5e7eb!important; border-radius:12px!important; }
|
| 678 |
+
.gr-chatbot .message.user { background:#eff6ff!important; border:1px solid #bfdbfe!important; color:#111827!important; font-size:.86rem!important; }
|
| 679 |
+
.gr-chatbot .message.bot { background:#fffbeb!important; border:1px solid #fde68a!important; color:#111827!important; font-size:.86rem!important; line-height:1.8!important; }
|
| 680 |
+
.gr-chatbot .message.bot strong { color:#1d4ed8!important; }
|
| 681 |
+
.gr-markdown { background:#fff!important; border:1.5px solid #e5e7eb!important; border-radius:8px!important;
|
| 682 |
+
padding:16px 20px!important; font-size:.85rem!important; line-height:1.75!important; color:#111827!important; }
|
| 683 |
+
.gr-markdown h2 { color:#1d4ed8!important; border-bottom:2px solid #fef3c7!important; padding-bottom:5px!important; }
|
| 684 |
+
.gr-markdown strong { color:#1d4ed8!important; }
|
| 685 |
+
.gr-markdown th { background:#1d4ed8!important; color:#fff!important; padding:8px 12px!important; }
|
| 686 |
+
.gr-markdown td { padding:7px 12px!important; border:1px solid #e5e7eb!important; color:#111827!important; }
|
| 687 |
+
.gr-markdown tr:nth-child(even) td { background:#f9fafb!important; }
|
| 688 |
+
.gr-markdown code { background:#eff6ff!important; color:#1d4ed8!important; border-radius:4px!important; padding:1px 6px!important; }
|
| 689 |
+
.gr-markdown pre { background:#1e293b!important; border-left:3px solid #1d4ed8!important; border-radius:8px!important; padding:14px!important; }
|
| 690 |
+
.gr-markdown pre code { background:transparent!important; color:#7dd3fc!important; }
|
| 691 |
+
::-webkit-scrollbar { width:5px; height:5px; }
|
| 692 |
+
::-webkit-scrollbar-thumb { background:#d1d5db; border-radius:3px; }
|
| 693 |
+
"""
|
| 694 |
+
|
| 695 |
+
# ββ UI BUILD βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 696 |
+
_ok = ollama_ok()
|
| 697 |
+
_models = ollama_models() if _ok else []
|
| 698 |
+
_chat_models = [m for m in _models if "embed" not in m.lower()] or [CHAT_MODEL]
|
| 699 |
+
_badge = f"π’ Ollama Running Β· {len(_models)} models" if _ok else "π΄ Ollama Offline"
|
| 700 |
+
|
| 701 |
+
with gr.Blocks(title="Manager Intelligence Agent") as demo:
|
| 702 |
+
gr.HTML(f"<style>{CSS}</style>")
|
| 703 |
+
HIST = gr.State([])
|
| 704 |
+
|
| 705 |
+
# HEADER
|
| 706 |
+
gr.HTML(f"""<div style="background:linear-gradient(135deg,#1e3a8a,#1d4ed8);padding:18px 32px 16px;border-bottom:3px solid #d97706">
|
| 707 |
+
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px">
|
| 708 |
+
<div style="display:flex;align-items:center;gap:14px">
|
| 709 |
+
<div style="width:46px;height:46px;background:rgba(255,255,255,.15);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px">π§ </div>
|
| 710 |
+
<div>
|
| 711 |
+
<div style="font-size:1.4rem;font-weight:800;color:#fff">Manager <span style="color:#fbbf24">Intelligence</span> Agent</div>
|
| 712 |
+
<div style="font-size:.68rem;color:rgba(255,255,255,.7);text-transform:uppercase;letter-spacing:.1em;margin-top:3px">Executive Operating System Β· 100% Offline Β· No data leaves your PC</div>
|
| 713 |
+
</div>
|
| 714 |
+
</div>
|
| 715 |
+
<div>
|
| 716 |
+
<div style="display:flex;gap:7px;flex-wrap:wrap;margin-bottom:7px">
|
| 717 |
+
<span style="background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);color:rgba(255,255,255,.9);padding:3px 11px;border-radius:50px;font-size:.68rem">π Offline</span>
|
| 718 |
+
<span style="background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);color:rgba(255,255,255,.9);padding:3px 11px;border-radius:50px;font-size:.68rem">π¦ Ollama</span>
|
| 719 |
+
<span style="background:rgba(251,191,36,.15);border:1px solid rgba(251,191,36,.4);color:#fbbf24;padding:3px 11px;border-radius:50px;font-size:.68rem">PDF Β· DOCX Β· XLSX Β· CSV Β· PPTX</span>
|
| 720 |
+
</div>
|
| 721 |
+
<span style="background:{'rgba(74,222,128,.15)' if _ok else 'rgba(248,113,113,.15)'};border:1px solid {'rgba(74,222,128,.5)' if _ok else 'rgba(248,113,113,.5)'};color:{'#4ade80' if _ok else '#fca5a5'};padding:5px 14px;border-radius:50px;font-size:.72rem;font-weight:600">{_badge}</span>
|
| 722 |
+
</div>
|
| 723 |
+
</div>
|
| 724 |
+
</div>""")
|
| 725 |
+
|
| 726 |
+
with gr.Tabs():
|
| 727 |
+
|
| 728 |
+
# ββ DASHBOARD βββββββββββββββββββββββββββββββββββββββββββββ
|
| 729 |
+
with gr.Tab("π Dashboard"):
|
| 730 |
+
dash = gr.HTML(dashboard_html())
|
| 731 |
+
gr.Button("π Refresh", variant="secondary").click(dashboard_html, outputs=[dash])
|
| 732 |
+
gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
|
| 733 |
+
border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-top:10px;line-height:1.65">
|
| 734 |
+
<strong>Getting started:</strong> Go to <strong>Documents</strong> tab β enter your folder path β click Scan & Index.
|
| 735 |
+
Then use <strong>Search</strong> to find files, <strong>Chat</strong> to ask questions, <strong>Email</strong> to draft messages.
|
| 736 |
+
</div>""")
|
| 737 |
+
|
| 738 |
+
# ββ SEARCH ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 739 |
+
with gr.Tab("π Search"):
|
| 740 |
+
gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
|
| 741 |
+
border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-bottom:14px">
|
| 742 |
+
<strong>Smart Search.</strong> Search by name, keyword, date, or topic.
|
| 743 |
+
After results appear, <strong>select a file</strong> from the dropdown, then click <strong>Open File</strong> or <strong>Show in Folder</strong>.
|
| 744 |
+
</div>""")
|
| 745 |
+
with gr.Row():
|
| 746 |
+
s_q = gr.Textbox(label="Search", placeholder='e.g. "Ahmed Al-Rashidi 2023" Β· "Q3 budget" Β· "contract renewal"', lines=1, scale=5)
|
| 747 |
+
s_btn = gr.Button("π Search", variant="primary", scale=1)
|
| 748 |
+
|
| 749 |
+
s_summary = gr.Markdown("*Enter a query and click Search.*")
|
| 750 |
+
s_html = gr.HTML("")
|
| 751 |
+
|
| 752 |
+
s_dd = gr.Dropdown(label="π Select a file from results above β then click Open or Show in Folder",
|
| 753 |
+
choices=[], value=None)
|
| 754 |
+
|
| 755 |
+
with gr.Row():
|
| 756 |
+
s_open = gr.Button("π Open File", variant="primary")
|
| 757 |
+
s_folder = gr.Button("ποΈ Show in Folder", variant="secondary")
|
| 758 |
+
|
| 759 |
+
s_status = gr.Textbox(label="Status", lines=1, interactive=False, value="")
|
| 760 |
+
|
| 761 |
+
def do_search_all(q):
|
| 762 |
+
html, choices, dd = run_search(q)
|
| 763 |
+
# summary extracted from html start
|
| 764 |
+
if choices:
|
| 765 |
+
summary = f"β
Found **{len(choices)} documents** for: *{q}*"
|
| 766 |
+
else:
|
| 767 |
+
summary = "*No results.*"
|
| 768 |
+
return html, summary, dd
|
| 769 |
+
|
| 770 |
+
s_btn.click(do_search_all, inputs=[s_q], outputs=[s_html, s_summary, s_dd])
|
| 771 |
+
s_q.submit(do_search_all, inputs=[s_q], outputs=[s_html, s_summary, s_dd])
|
| 772 |
+
s_open.click(open_file, inputs=[s_dd], outputs=[s_status])
|
| 773 |
+
s_folder.click(show_in_folder, inputs=[s_dd], outputs=[s_status])
|
| 774 |
+
|
| 775 |
+
# ββ CHAT ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 776 |
+
with gr.Tab("π¬ Chat & Intelligence"):
|
| 777 |
+
with gr.Row():
|
| 778 |
+
with gr.Column(scale=1, min_width=260):
|
| 779 |
+
c_model = gr.Dropdown(label="Model", choices=_chat_models, value=_chat_models[0])
|
| 780 |
+
c_focus = gr.Dropdown(label="Focus on file (optional)",
|
| 781 |
+
choices=["β All Documents β"] + all_names(), value="β All Documents β")
|
| 782 |
+
gr.Button("π Refresh Files", variant="secondary").click(
|
| 783 |
+
lambda: gr.Dropdown(choices=["β All Documents β"] + all_names()), outputs=[c_focus])
|
| 784 |
+
gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
|
| 785 |
+
border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-top:12px;line-height:1.8">
|
| 786 |
+
<strong>Try asking:</strong><br>
|
| 787 |
+
β’ Find all records for Ahmed Hassan<br>
|
| 788 |
+
β’ Summarize the Q3 financial report<br>
|
| 789 |
+
β’ List all salary changes 2020β2024<br>
|
| 790 |
+
β’ Who approved the merger?<br>
|
| 791 |
+
β’ What contracts expire this year?
|
| 792 |
+
</div>""")
|
| 793 |
+
gr.HTML("<div style='font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin:16px 0 8px'>β‘ Document Analysis</div>")
|
| 794 |
+
a_file = gr.Dropdown(label="Select document", choices=all_names())
|
| 795 |
+
gr.Button("π", variant="secondary").click(lambda: gr.Dropdown(choices=all_names()), outputs=[a_file])
|
| 796 |
+
a_btn = gr.Button("π Full Analysis", variant="primary")
|
| 797 |
+
|
| 798 |
+
with gr.Column(scale=3):
|
| 799 |
+
chatbot = gr.Chatbot(label="", height=460, show_label=False)
|
| 800 |
+
with gr.Row():
|
| 801 |
+
c_in = gr.Textbox(label="", show_label=False,
|
| 802 |
+
placeholder="Ask anything about your documents...", lines=2, scale=5)
|
| 803 |
+
with gr.Column(scale=1, min_width=90):
|
| 804 |
+
c_send = gr.Button("Send β", variant="primary")
|
| 805 |
+
c_clear = gr.Button("Clear", variant="secondary")
|
| 806 |
+
|
| 807 |
+
def chat_fn(msg, hist, model, focus):
|
| 808 |
+
new_hist, _ = do_chat(msg, hist, model, focus)
|
| 809 |
+
return new_hist, "", new_hist
|
| 810 |
+
|
| 811 |
+
c_send.click(chat_fn, inputs=[c_in, HIST, c_model, c_focus], outputs=[HIST, c_in, chatbot])
|
| 812 |
+
c_in.submit(chat_fn, inputs=[c_in, HIST, c_model, c_focus], outputs=[HIST, c_in, chatbot])
|
| 813 |
+
c_clear.click(lambda: ([], []), outputs=[HIST, chatbot])
|
| 814 |
+
a_btn.click(do_analyze, inputs=[a_file, c_model], outputs=[chatbot, HIST])
|
| 815 |
+
|
| 816 |
+
# ββ EMAIL βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 817 |
+
with gr.Tab("βοΈ Email Drafts"):
|
| 818 |
+
gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
|
| 819 |
+
border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-bottom:14px">
|
| 820 |
+
<strong>AI Email Drafting.</strong> Describe what the email should say β choose tone β click Draft Email.
|
| 821 |
+
A complete professional email is written instantly. Copy it and send.
|
| 822 |
+
</div>""")
|
| 823 |
+
with gr.Row():
|
| 824 |
+
with gr.Column(scale=1):
|
| 825 |
+
e_model = gr.Dropdown(label="Model", choices=_chat_models, value=_chat_models[0])
|
| 826 |
+
e_tone = gr.Dropdown(label="Tone",
|
| 827 |
+
choices=["Formal & Executive","Professional & Warm","Concise & Direct","Diplomatic"],
|
| 828 |
+
value="Formal & Executive")
|
| 829 |
+
e_doc = gr.Dropdown(label="Reference document (optional)",
|
| 830 |
+
choices=["β None β"] + all_names(), value="β None β")
|
| 831 |
+
gr.Button("π Refresh", variant="secondary").click(
|
| 832 |
+
lambda: gr.Dropdown(choices=["β None β"] + all_names()), outputs=[e_doc])
|
| 833 |
+
gr.HTML("""<div style="background:#fffbeb;border:1px solid #fde68a;border-left:4px solid #d97706;
|
| 834 |
+
border-radius:8px;padding:11px 15px;font-size:.80rem;color:#92400e;margin-top:12px;line-height:1.8">
|
| 835 |
+
<strong>Examples:</strong><br>
|
| 836 |
+
β’ Request HR approval for new hire<br>
|
| 837 |
+
β’ Follow up on contract with Supplier X<br>
|
| 838 |
+
β’ Share Q3 results with the board<br>
|
| 839 |
+
β’ Invite team to strategy meeting
|
| 840 |
+
</div>""")
|
| 841 |
+
with gr.Column(scale=2):
|
| 842 |
+
e_inst = gr.Textbox(label="Email instructions",
|
| 843 |
+
placeholder="Example: Write an email to HR requesting approval to hire 2 new engineers for the AI team. Reference the project timeline document.",
|
| 844 |
+
lines=5)
|
| 845 |
+
e_btn = gr.Button("βοΈ Draft Email", variant="primary")
|
| 846 |
+
e_out = gr.Textbox(label="Email Draft β copy and send", lines=20, max_lines=35,
|
| 847 |
+
placeholder="Your email appears here after clicking Draft Email...")
|
| 848 |
+
e_btn.click(do_email, inputs=[e_inst, e_doc, e_tone, e_model], outputs=[e_out])
|
| 849 |
+
|
| 850 |
+
# ββ TASKS & CALENDAR ββββββββββββββββββββββββββββββββββββββ
|
| 851 |
+
with gr.Tab("π Tasks & Calendar"):
|
| 852 |
+
with gr.Row():
|
| 853 |
+
with gr.Column(scale=1):
|
| 854 |
+
gr.HTML("<div style='font-size:.95rem;font-weight:700;color:#111827;margin-bottom:10px'>π Task Manager</div>")
|
| 855 |
+
with gr.Row():
|
| 856 |
+
t_txt = gr.Textbox(label="Task", placeholder="What needs to be done?", scale=3)
|
| 857 |
+
t_due = gr.Textbox(label="Due date (YYYY-MM-DD)", placeholder="2025-12-31", scale=2)
|
| 858 |
+
with gr.Row():
|
| 859 |
+
t_pri = gr.Dropdown(label="Priority", choices=["high","medium","low"], value="medium", scale=1)
|
| 860 |
+
t_note = gr.Textbox(label="Note", placeholder="Optional context", scale=2)
|
| 861 |
+
with gr.Row():
|
| 862 |
+
t_add = gr.Button("β Add Task", variant="primary")
|
| 863 |
+
t_msg = gr.Markdown("")
|
| 864 |
+
t_disp = gr.HTML(tasks_html())
|
| 865 |
+
with gr.Row():
|
| 866 |
+
t_idx = gr.Textbox(label="Task # (from list)", placeholder="0", scale=1)
|
| 867 |
+
gr.Button("β
Toggle Done", variant="secondary", scale=1).click(
|
| 868 |
+
toggle_task, inputs=[t_idx], outputs=[t_disp, t_msg])
|
| 869 |
+
gr.Button("ποΈ Delete", variant="secondary", scale=1).click(
|
| 870 |
+
delete_task, inputs=[t_idx], outputs=[t_disp, t_msg])
|
| 871 |
+
|
| 872 |
+
with gr.Column(scale=1):
|
| 873 |
+
gr.HTML("<div style='font-size:.95rem;font-weight:700;color:#111827;margin-bottom:10px'>ποΈ Calendar & Events</div>")
|
| 874 |
+
with gr.Row():
|
| 875 |
+
ev_t = gr.Textbox(label="Event title", placeholder="Meeting / Deadline", scale=3)
|
| 876 |
+
ev_d = gr.Textbox(label="Date (YYYY-MM-DD)", placeholder="2025-12-31", scale=2)
|
| 877 |
+
with gr.Row():
|
| 878 |
+
ev_time = gr.Textbox(label="Time", placeholder="14:00", scale=1)
|
| 879 |
+
ev_note = gr.Textbox(label="Note", placeholder="Location, agenda", scale=2)
|
| 880 |
+
with gr.Row():
|
| 881 |
+
ev_add = gr.Button("π
Add Event", variant="primary")
|
| 882 |
+
ev_msg = gr.Markdown("")
|
| 883 |
+
ev_disp = gr.HTML(events_html())
|
| 884 |
+
with gr.Row():
|
| 885 |
+
ev_idx = gr.Textbox(label="Event # to delete", placeholder="0", scale=1)
|
| 886 |
+
gr.Button("ποΈ Delete Event", variant="secondary", scale=2).click(
|
| 887 |
+
delete_event, inputs=[ev_idx], outputs=[ev_disp, ev_msg])
|
| 888 |
+
|
| 889 |
+
t_add.click(add_task, inputs=[t_txt, t_due, t_pri, t_note],
|
| 890 |
+
outputs=[t_disp, t_msg, t_txt, t_due, t_pri, t_note])
|
| 891 |
+
ev_add.click(add_event, inputs=[ev_t, ev_d, ev_time, ev_note],
|
| 892 |
+
outputs=[ev_disp, ev_msg, ev_t, ev_d, ev_time, ev_note])
|
| 893 |
+
|
| 894 |
+
# ββ DOCUMENTS βββββββββββββββββββββββββββββββββββββββββββββ
|
| 895 |
+
with gr.Tab("π Documents"):
|
| 896 |
+
with gr.Tabs():
|
| 897 |
+
with gr.Tab("π Index & Scan"):
|
| 898 |
+
gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
|
| 899 |
+
border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-bottom:10px">
|
| 900 |
+
<strong>Index your files.</strong> Enter your folder path and click Scan. Files are indexed permanently β re-scanning only picks up new files.
|
| 901 |
+
</div>""")
|
| 902 |
+
with gr.Row():
|
| 903 |
+
with gr.Column(scale=1):
|
| 904 |
+
f_flds = gr.Textbox(label="Folders to scan (one per line)",
|
| 905 |
+
value="\n".join(WATCH_FOLDERS), lines=5)
|
| 906 |
+
with gr.Row():
|
| 907 |
+
f_scan = gr.Button("π Scan & Index", variant="primary")
|
| 908 |
+
f_clr = gr.Button("ποΈ Clear Index", variant="secondary")
|
| 909 |
+
gr.HTML("""<div style="background:#fffbeb;border:1px solid #fde68a;border-left:4px solid #d97706;
|
| 910 |
+
border-radius:8px;padding:10px 14px;font-size:.80rem;color:#92400e;margin-top:8px">
|
| 911 |
+
β‘ First full scan may take 10β30 min depending on folder size.
|
| 912 |
+
</div>""")
|
| 913 |
+
f_up = gr.File(label="Or upload specific files", file_count="multiple",
|
| 914 |
+
file_types=[".pdf",".docx",".doc",".xlsx",".xls",".csv",".txt",".pptx"])
|
| 915 |
+
f_upbtn= gr.Button("β‘ Index Uploaded Files", variant="secondary")
|
| 916 |
+
with gr.Column(scale=1):
|
| 917 |
+
f_log = gr.Markdown("*Scan results appear here.*")
|
| 918 |
+
f_stats = gr.Markdown(lib_stats())
|
| 919 |
+
f_scan.click(do_scan, inputs=[f_flds], outputs=[f_log, f_stats])
|
| 920 |
+
f_clr.click(do_clear, outputs=[f_log, f_stats])
|
| 921 |
+
f_upbtn.click(do_upload, inputs=[f_up], outputs=[f_log, f_stats])
|
| 922 |
+
|
| 923 |
+
with gr.Tab("π Preview & Open"):
|
| 924 |
+
with gr.Row():
|
| 925 |
+
p_sel = gr.Dropdown(label="Select document", choices=all_names(), scale=4)
|
| 926 |
+
p_load = gr.Button("π Load", variant="primary", scale=1)
|
| 927 |
+
gr.Button("π", variant="secondary", scale=1).click(
|
| 928 |
+
lambda: gr.Dropdown(choices=all_names()), outputs=[p_sel])
|
| 929 |
+
p_info = gr.Markdown("*Select a file and click Load.*")
|
| 930 |
+
with gr.Row():
|
| 931 |
+
p_open = gr.Button("π Open File", variant="primary")
|
| 932 |
+
p_loc = gr.Button("ποΈ Show in Explorer", variant="secondary")
|
| 933 |
+
p_stat = gr.Textbox(label="Status", lines=1, interactive=False, value="")
|
| 934 |
+
p_text = gr.Textbox(label="Document content", lines=28, max_lines=60,
|
| 935 |
+
placeholder="Full text appears here after loading...")
|
| 936 |
+
p_load.click(do_load, inputs=[p_sel], outputs=[p_info, p_text])
|
| 937 |
+
p_open.click(open_indexed, inputs=[p_sel], outputs=[p_stat])
|
| 938 |
+
p_loc.click(locate_indexed, inputs=[p_sel], outputs=[p_stat])
|
| 939 |
+
|
| 940 |
+
# ββ SETUP βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 941 |
+
with gr.Tab("βοΈ Setup"):
|
| 942 |
+
gr.Markdown(f"""
|
| 943 |
+
## Setup Guide
|
| 944 |
+
|
| 945 |
+
### 1. Install Ollama
|
| 946 |
+
Download from **ollama.com** β install β runs on `localhost:11434`
|
| 947 |
+
|
| 948 |
+
### 2. Install Models (open Command Prompt and run)
|
| 949 |
+
```
|
| 950 |
+
ollama pull nomic-embed-text
|
| 951 |
+
ollama pull llama3
|
| 952 |
+
```
|
| 953 |
+
|
| 954 |
+
### 3. Edit config at top of app.py
|
| 955 |
+
```python
|
| 956 |
+
WATCH_FOLDERS = [r"D:\\"]
|
| 957 |
+
CHAT_MODEL = "llama3"
|
| 958 |
+
```
|
| 959 |
+
|
| 960 |
+
### 4. Run
|
| 961 |
+
```
|
| 962 |
+
python app.py
|
| 963 |
+
```
|
| 964 |
+
|
| 965 |
+
---
|
| 966 |
+
|
| 967 |
+
| Model | Size | Best For |
|
| 968 |
+
|-------|------|---------|
|
| 969 |
+
| phi3 | 2.3 GB | Low RAM / fast |
|
| 970 |
+
| mistral | 4.1 GB | General |
|
| 971 |
+
| llama3 | 4.7 GB | β Recommended |
|
| 972 |
+
| gemma2 | 5.4 GB | Deep analysis |
|
| 973 |
+
|
| 974 |
+
---
|
| 975 |
+
|
| 976 |
+
| Problem | Fix |
|
| 977 |
+
|---------|-----|
|
| 978 |
+
| Ollama offline | Run `ollama serve` in terminal |
|
| 979 |
+
| Model missing | Run `ollama pull llama3` |
|
| 980 |
+
| Slow responses | Switch to `phi3` |
|
| 981 |
+
| File won't open | Verify file still exists on disk |
|
| 982 |
+
| D:\\ not scanning | Use double backslash: `D:\\\\` |
|
| 983 |
+
""")
|
| 984 |
+
with gr.Row():
|
| 985 |
+
st_btn = gr.Button("π Check Ollama", variant="primary")
|
| 986 |
+
st_out = gr.Markdown(f"**Status:** {_badge}")
|
| 987 |
+
st_btn.click(
|
| 988 |
+
lambda: f"**Status:** {'π’ Running Β· ' + str(len(ollama_models())) + ' models' if ollama_ok() else 'π΄ Offline β run: ollama serve'}",
|
| 989 |
+
outputs=[st_out])
|
| 990 |
+
|
| 991 |
+
gr.HTML("""<div style="background:#fff;border-top:1px solid #e5e7eb;padding:12px 32px;text-align:center;font-size:.70rem;color:#9ca3af">
|
| 992 |
+
<span style="color:#1d4ed8;font-weight:700">Manager Intelligence Agent</span> Β· 100% Offline Β· Ollama + FAISS Β· Index stored at ~/manager_agent_index/
|
| 993 |
+
</div>""")
|
| 994 |
+
|
| 995 |
+
# ββ LAUNCH βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 996 |
+
if __name__ == "__main__":
|
| 997 |
+
print("\n" + "="*55)
|
| 998 |
+
print(" Manager Intelligence Agent")
|
| 999 |
+
print("="*55)
|
| 1000 |
+
print(f" Ollama: {'β
Running' if ollama_ok() else 'β Not running β start Ollama first'}")
|
| 1001 |
+
print(f" Models: {', '.join(ollama_models()) or 'None installed'}")
|
| 1002 |
+
print(f" Index: {INDEX_DIR}")
|
| 1003 |
+
print(f" URL: http://localhost:7860")
|
| 1004 |
+
print("="*55 + "\n")
|
| 1005 |
+
demo.launch(server_name="127.0.0.1", server_port=7860, inbrowser=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0,<5.0.0
|
| 2 |
+
faiss-cpu>=1.7.4
|
| 3 |
+
pdfplumber>=0.10.0
|
| 4 |
+
python-docx>=1.1.0
|
| 5 |
+
pandas>=2.0.0
|
| 6 |
+
openpyxl>=3.1.0
|
| 7 |
+
numpy>=1.24.0
|
| 8 |
+
requests>=2.28.0
|
| 9 |
+
python-pptx>=0.6.21
|