Universities commited on
Commit
d789b06
Β·
verified Β·
1 Parent(s): a617bb2

Upload 3 files

Browse files
Files changed (3) hide show
  1. START.bat +29 -0
  2. app.py +1005 -0
  3. 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','')} &nbsp;Β·&nbsp; {m.get('words',0):,} words &nbsp;Β·&nbsp; {m.get('ftype','')} &nbsp;Β·&nbsp; {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