Spaces:
Build error
Build error
| """ | |
| Manager Intelligence Agent | |
| Free HF Inference API (Llama-3-8B) β no paid API key needed | |
| Deploy on Hugging Face Spaces (Gradio) | |
| """ | |
| import os, re, json, shutil, pickle, hashlib, datetime, logging | |
| from pathlib import Path | |
| import gradio as gr | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| log = logging.getLogger(__name__) | |
| # ββ CONFIG βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| INDEX_DIR = os.path.join(os.path.expanduser("~"), "manager_agent_index") | |
| TASKS_FILE = os.path.join(INDEX_DIR, "_tasks.json") | |
| EVENTS_FILE = os.path.join(INDEX_DIR, "_events.json") | |
| os.makedirs(INDEX_DIR, exist_ok=True) | |
| SUPPORTED = {".pdf",".docx",".doc",".xlsx",".xls",".csv",".txt",".eml",".rtf",".pptx",".ppt"} | |
| MAX_MB = 50 | |
| ICONS = {".pdf":"π",".docx":"π",".doc":"π",".xlsx":"π",".xls":"π", | |
| ".csv":"π",".pptx":"π",".ppt":"π",".txt":"π",".eml":"π§",".msg":"π§"} | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") # optional β removes rate limits | |
| HF_MODEL = "meta-llama/Meta-Llama-3-8B-Instruct" | |
| # ββ AI BACKEND βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_client(): | |
| try: | |
| from huggingface_hub import InferenceClient | |
| return InferenceClient(model=HF_MODEL, token=HF_TOKEN if HF_TOKEN else None) | |
| except Exception as e: | |
| log.error(f"InferenceClient init: {e}") | |
| return None | |
| def ai_status(): | |
| try: | |
| from huggingface_hub import InferenceClient | |
| return "β HF Inference (Llama-3-8B) β Free" | |
| except: | |
| return "β οΈ huggingface_hub not installed" | |
| def call_llm(prompt, system=None, history=None): | |
| client = get_client() | |
| if not client: | |
| return "β Could not connect to HF Inference API." | |
| messages = [] | |
| if system: | |
| messages.append({"role": "system", "content": system}) | |
| if history: | |
| for u, b in history[-6:]: | |
| messages += [{"role":"user","content":str(u)}, | |
| {"role":"assistant","content":str(b)}] | |
| messages.append({"role": "user", "content": prompt}) | |
| try: | |
| resp = client.chat_completion(messages=messages, max_tokens=1024, temperature=0.7) | |
| return resp.choices[0].message.content.strip() | |
| except Exception as e: | |
| log.error(f"call_llm: {e}") | |
| return f"β Inference error: {e}" | |
| def do_generate(prompt): | |
| return call_llm(prompt) | |
| def do_chat_llm(prompt, context, history): | |
| system = ("You are an elite executive assistant AI with access to the manager's document archive. " | |
| "Answer precisely using the provided context. Cite document names. " | |
| "Use bullet points. Flag deadlines and action items proactively.") | |
| return call_llm(f"{prompt}\n\n--- Document Context ---\n{context}", system=system, history=history) | |
| # ββ TEXT EXTRACTION ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract(fp): | |
| ext = Path(fp).suffix.lower() | |
| try: | |
| if ext == ".pdf": | |
| import pdfplumber | |
| with pdfplumber.open(fp) as pdf: | |
| return "\n".join(p.extract_text() or "" for p in pdf.pages) | |
| if ext in (".docx", ".doc"): | |
| from docx import Document | |
| doc = Document(fp) | |
| parts = [p.text for p in doc.paragraphs if p.text.strip()] | |
| for t in doc.tables: | |
| for row in t.rows: | |
| parts.append(" | ".join(c.text.strip() for c in row.cells if c.text.strip())) | |
| return "\n".join(parts) | |
| if ext in (".xlsx", ".xls"): | |
| import pandas as pd | |
| xl = pd.ExcelFile(fp) | |
| return "\n\n".join(f"[{s}]\n{xl.parse(s).head(200).to_string(index=False)}" for s in xl.sheet_names) | |
| if ext == ".csv": | |
| import pandas as pd | |
| return pd.read_csv(fp, encoding="utf-8", errors="ignore").to_string(index=False) | |
| if ext in (".pptx", ".ppt"): | |
| from pptx import Presentation | |
| prs = Presentation(fp) | |
| return "\n".join(" ".join(s.text for s in sl.shapes if hasattr(s,"text")) for sl in prs.slides) | |
| return open(fp, "r", encoding="utf-8", errors="ignore").read() | |
| except Exception as e: | |
| log.warning(f"extract({fp}): {e}") | |
| return "" | |
| # ββ INDEXING βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fhash(fp): | |
| return hashlib.md5(f"{fp}{os.path.getmtime(fp)}".encode()).hexdigest()[:12] | |
| def is_indexed(fp): | |
| return os.path.exists(f"{INDEX_DIR}/{fhash(fp)}.pkl") | |
| def make_chunks(text, fname, size=350, overlap=70): | |
| words = re.sub(r'\s+', ' ', text).strip().split() | |
| chunks = [] | |
| for s in range(0, len(words), size - overlap): | |
| e = min(s + size, len(words)) | |
| c = " ".join(words[s:e]) | |
| if len(c) > 50: | |
| chunks.append({"text": c, "source": fname, "preview": c[:200]}) | |
| if e == len(words): break | |
| return chunks | |
| def index_file(fp): | |
| fname = Path(fp).name | |
| try: | |
| size_mb = os.path.getsize(fp) / (1024*1024) | |
| except Exception as e: | |
| return False, f"Cannot read: {e}" | |
| if size_mb > MAX_MB: | |
| return False, f">{MAX_MB}MB skipped" | |
| text = extract(fp) | |
| if not text or len(text.strip()) < 30: | |
| return False, "No text extracted" | |
| chunks = make_chunks(text, fname) | |
| if not chunks: | |
| return False, "No chunks" | |
| fh = fhash(fp) | |
| meta = {"filename": fname, "filepath": str(fp), | |
| "ftype": Path(fp).suffix.upper().strip("."), | |
| "words": len(text.split()), | |
| "mb": round(size_mb, 2), | |
| "date": datetime.datetime.fromtimestamp(os.path.getmtime(fp)).strftime("%Y-%m-%d")} | |
| with open(f"{INDEX_DIR}/{fh}.pkl", "wb") as f: | |
| pickle.dump({"chunks": chunks, "meta": meta}, f) | |
| return True, f"{len(chunks)} chunks indexed" | |
| # ββ SEARCH βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_all(): | |
| chunks = [] | |
| for ff in os.listdir(INDEX_DIR): | |
| if not ff.endswith(".pkl") or ff.startswith("_"): | |
| continue | |
| try: | |
| with open(f"{INDEX_DIR}/{ff}", "rb") as f: | |
| data = pickle.load(f) | |
| for c in data["chunks"]: | |
| c = c.copy(); c["meta"] = data["meta"]; chunks.append(c) | |
| except Exception as e: | |
| log.warning(f"load_all {ff}: {e}") | |
| return chunks | |
| def keyword_search(query, chunks): | |
| stop = {"the","a","an","is","in","on","at","to","for","of","and","or","it","was","are","with","this","that"} | |
| kws = {w.lower() for w in re.findall(r'\w+', query) if len(w) > 2} - stop | |
| if not kws: return {} | |
| seen = {} | |
| for c in chunks: | |
| kh = sum(1 for kw in kws if kw in c["text"].lower()) | |
| if kh == 0: continue | |
| fn = c["source"] | |
| if fn not in seen or seen[fn]["kw"] < kh: | |
| seen[fn] = {"chunk": c, "score": kh * 0.1, "kw": kh} | |
| return seen | |
| def run_search(query): | |
| EMPTY = gr.Dropdown(choices=[], value=None, label="Select result") | |
| if not query.strip(): | |
| return "<p style='color:#6b7280;padding:20px;text-align:center'>Enter a search query.</p>", [], EMPTY | |
| chunks = load_all() | |
| if not chunks: | |
| return "<p style='color:#dc2626;padding:20px'>β No files indexed. Upload files in Documents tab.</p>", [], EMPTY | |
| stop = {"the","a","an","is","in","on","at","to","for","of","and","or","it","was","are","with","this","that"} | |
| kws = {w.lower() for w in re.findall(r'\w+', query) if len(w) > 2} - stop | |
| seen = keyword_search(query, chunks) | |
| if not seen: | |
| all_fnames = list(dict.fromkeys(c["source"] for c in chunks)) | |
| cards = "".join(f"""<div style="background:#fff;border:1.5px solid #d1d5db;border-left:4px solid #6b7280;border-radius:8px;padding:12px 16px;margin-bottom:8px"> | |
| <div style="font-weight:700;color:#374151">π {fn}</div></div>""" for fn in all_fnames) | |
| return (f"<div style='background:#fffbeb;border:1px solid #fde68a;border-left:4px solid #d97706;border-radius:8px;padding:14px 18px;margin-bottom:12px'>" | |
| f"<strong>No matches for: <em>{query}</em></strong><br>Try shorter words.</div>" | |
| f"<p style='font-size:.85rem;color:#374151;margin-bottom:10px'><strong>All indexed files ({len(all_fnames)}):</strong></p>" + cards, | |
| all_fnames, gr.Dropdown(choices=all_fnames, value=None, label="Select a file")) | |
| results = sorted(seen.items(), key=lambda x: -x[1]["score"])[:8] | |
| html = f"<p style='color:#374151;margin-bottom:12px;font-size:.85rem'>β <strong>{len(results)} documents</strong> for: <em>{query}</em></p>" | |
| choices = [] | |
| for fname, v in results: | |
| m = v["chunk"]["meta"] | |
| icon = ICONS.get("." + m.get("ftype","").lower(), "π") | |
| prev = v["chunk"]["preview"] | |
| for kw in kws: | |
| 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) | |
| html += f"""<div style="background:#fff;border:1.5px solid #d1d5db;border-left:5px solid #1d4ed8; | |
| border-radius:10px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.07)"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> | |
| <div style="display:flex;gap:10px;align-items:center"> | |
| <span style="font-size:1.4rem">{icon}</span> | |
| <div> | |
| <div style="font-size:.92rem;font-weight:700;color:#111827">{fname}</div> | |
| <div style="font-size:.68rem;color:#9ca3af">{m.get("ftype","")} Β· {m.get("mb",0)} MB Β· {m.get("words",0):,} words Β· {m.get("date","")}</div> | |
| </div> | |
| </div> | |
| <span style="background:#eff6ff;border:1px solid #bfdbfe;color:#1d4ed8;padding:3px 10px;border-radius:50px;font-size:.72rem;font-weight:700">π {v["kw"]} hits</span> | |
| </div> | |
| <div style="font-size:.82rem;color:#374151;line-height:1.65;border-top:1px solid #f3f4f6;padding-top:8px">{prev}β¦</div> | |
| </div>""" | |
| choices.append(fname) | |
| return html, choices, gr.Dropdown(choices=choices, value=choices[0], label="π Select a file to preview") | |
| # ββ DOCUMENT HELPERS βββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_text(fname): | |
| for ff in os.listdir(INDEX_DIR): | |
| if not ff.endswith(".pkl"): continue | |
| try: | |
| with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f) | |
| if data["meta"]["filename"] == fname: | |
| return "\n\n".join(c["text"] for c in data["chunks"]) | |
| except: pass | |
| return "" | |
| def all_meta(): | |
| docs = [] | |
| for ff in os.listdir(INDEX_DIR): | |
| if not ff.endswith(".pkl"): continue | |
| try: | |
| with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f) | |
| docs.append(data["meta"]) | |
| except: pass | |
| return sorted(docs, key=lambda x: x.get("date",""), reverse=True) | |
| def all_names(): return [d["filename"] for d in all_meta()] | |
| def lib_stats(): | |
| docs = all_meta() | |
| if not docs: return "*No files indexed yet.*" | |
| tw = sum(d.get("words",0) for d in docs) | |
| lines = [f"**π {len(docs)} files Β· {tw:,} words**\n"] | |
| for d in docs[:40]: | |
| icon = ICONS.get("." + d.get("ftype","").lower(), "π") | |
| lines.append(f"{icon} **{d['filename']}** Β· {d.get('words',0):,}w Β· {d.get('date','')} Β· {d.get('ftype','')}") | |
| if len(docs) > 40: lines.append(f"*...and {len(docs)-40} more*") | |
| return "\n".join(lines) | |
| # ββ TASKS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_tasks(): | |
| try: | |
| if os.path.exists(TASKS_FILE): | |
| with open(TASKS_FILE) as f: return json.load(f) | |
| except: pass | |
| return [] | |
| def save_tasks(t): | |
| with open(TASKS_FILE, "w") as f: json.dump(t, f, indent=2) | |
| def tasks_html(): | |
| tasks = load_tasks(); today = datetime.date.today().isoformat() | |
| if not tasks: | |
| return "<p style='padding:20px;text-align:center;color:#6b7280;background:#f9fafb;border:2px dashed #d1d5db;border-radius:8px'>No tasks yet.</p>" | |
| rows = "" | |
| for i, t in enumerate(tasks): | |
| done = t.get("done", False) | |
| due = t.get("due", "") | |
| ov = due and due < today and not done | |
| bg = "#fef2f2" if ov else ("#f9fafb" if done else "#fff") | |
| bl = "#dc2626" if ov else ("#d1d5db" if done else "#1d4ed8") | |
| pri = t.get("priority","medium") | |
| pc = {"high":"#dc2626","medium":"#d97706","low":"#15803d"}.get(pri,"#6b7280") | |
| rows += f"""<div style="display:flex;align-items:center;gap:10px;background:{bg}; | |
| border:1px solid #e5e7eb;border-left:4px solid {bl};border-radius:8px;padding:11px 14px;opacity:{'0.55' if done else '1'};margin-bottom:7px"> | |
| <span style="font-family:monospace;font-size:.68rem;color:#9ca3af;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:4px;padding:1px 6px;flex-shrink:0">#{i}</span> | |
| <div style="flex:1"> | |
| <div style="font-size:.85rem;font-weight:500;color:#111827;{'text-decoration:line-through;color:#9ca3af' if done else ''}">{t['text']}</div> | |
| {f'<div style="font-size:.70rem;color:{"#dc2626" if ov else "#9ca3af"};margin-top:2px">π {due}{" β οΈ OVERDUE" if ov else ""}</div>' if due else ''} | |
| </div> | |
| <span style="font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:50px;color:{pc};border:1px solid {pc}40">{pri.upper()}</span> | |
| </div>""" | |
| return rows + "<p style='font-size:.70rem;color:#9ca3af;text-align:center;margin-top:4px;font-style:italic'>Use task # to toggle/delete</p>" | |
| # ββ EVENTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_events(): | |
| try: | |
| if os.path.exists(EVENTS_FILE): | |
| with open(EVENTS_FILE) as f: return json.load(f) | |
| except: pass | |
| return [] | |
| def save_events(e): | |
| with open(EVENTS_FILE, "w") as f: json.dump(e, f, indent=2) | |
| def events_html(): | |
| evs = load_events(); today = datetime.date.today().isoformat() | |
| up = sorted([e for e in evs if e.get("date","") >= today], key=lambda x: x["date"]) | |
| past = sorted([e for e in evs if e.get("date","") < today], key=lambda x: x["date"], reverse=True)[:3] | |
| if not up and not past: | |
| return "<p style='padding:20px;text-align:center;color:#6b7280;background:#f9fafb;border:2px dashed #d1d5db;border-radius:8px'>No events yet.</p>" | |
| def row(e, old=False): | |
| try: day=datetime.datetime.strptime(e["date"],"%Y-%m-%d").strftime("%d"); mon=datetime.datetime.strptime(e["date"],"%Y-%m-%d").strftime("%b %Y") | |
| except: day=e.get("date",""); mon="" | |
| return f"""<div style="display:flex;align-items:center;gap:12px;background:{'#f9fafb' if old else '#fff'}; | |
| border:1px solid #e5e7eb;border-radius:8px;padding:11px 14px;margin-bottom:7px;opacity:{'0.45' if old else '1'}"> | |
| <div style="text-align:center;background:#eff6ff;border-radius:6px;padding:6px 10px;min-width:50px;flex-shrink:0"> | |
| <div style="font-size:1.3rem;font-weight:800;color:#1d4ed8;line-height:1">{day}</div> | |
| <div style="font-size:.58rem;color:#3b82f6;text-transform:uppercase">{mon}</div> | |
| </div> | |
| <div> | |
| <div style="font-size:.85rem;font-weight:600;color:#111827">{e['title']}</div> | |
| {f'<div style="font-size:.70rem;color:#6b7280;margin-top:2px">π {e["time"]}</div>' if e.get("time") else ''} | |
| {f'<div style="font-size:.70rem;color:#9ca3af;font-style:italic">{e["note"]}</div>' if e.get("note") else ''} | |
| </div> | |
| </div>""" | |
| html = "" | |
| if up: | |
| html += "<p style='font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px'>π Upcoming</p>" | |
| html += "".join(row(e) for e in up[:10]) | |
| if past: | |
| html += "<p style='font-size:.72rem;font-weight:700;color:#9ca3af;text-transform:uppercase;letter-spacing:.08em;margin:10px 0 6px'>Past</p>" | |
| html += "".join(row(e, True) for e in past) | |
| return html | |
| # ββ DASHBOARD ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def dashboard_html(): | |
| tasks = load_tasks(); today = datetime.date.today().isoformat() | |
| today_s = datetime.date.today().strftime("%A, %B %d, %Y") | |
| hr = datetime.datetime.now().hour | |
| greet = "Good morning" if hr < 12 else "Good afternoon" if hr < 17 else "Good evening" | |
| pending = [t for t in tasks if not t.get("done")] | |
| overdue = [t for t in pending if t.get("due","") and t["due"] < today] | |
| hi = [t for t in pending if t.get("priority") == "high"] | |
| evs = load_events() | |
| up = sorted([e for e in evs if e.get("date","") >= today], key=lambda x: x["date"])[:5] | |
| docs = all_meta(); recent = docs[:6] | |
| def stat(n, lbl, bg, tc, bc): | |
| 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)"> | |
| <div style="font-size:1.9rem;font-weight:800;color:{tc};line-height:1">{n}</div> | |
| <div style="font-size:.70rem;color:{tc};font-weight:600;text-transform:uppercase;letter-spacing:.07em;margin-top:5px;opacity:.8">{lbl}</div> | |
| </div>""" | |
| stats = f"""<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:20px"> | |
| {stat(len(docs),"Indexed Docs","#eff6ff","#1d4ed8","#bfdbfe")} | |
| {stat(len(pending),"Pending Tasks","#fffbeb","#d97706","#fde68a")} | |
| {stat(len(overdue),"Overdue","#fef2f2" if overdue else "#f0fdf4","#dc2626" if overdue else "#15803d","#fecaca" if overdue else "#bbf7d0")} | |
| {stat(len(hi),"High Priority","#fef2f2" if hi else "#f0fdf4","#dc2626" if hi else "#15803d","#fecaca" if hi else "#bbf7d0")} | |
| {stat(len(up),"Upcoming Events","#f5f3ff","#7c3aed","#ddd6fe")} | |
| </div>""" | |
| def card(title, rows_html, empty_msg): | |
| 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)"> | |
| <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> | |
| {rows_html or f'<p style="color:#9ca3af;font-size:.80rem;text-align:center;padding:12px 0">{empty_msg}</p>'} | |
| </div>""" | |
| def drow(icon, text, meta): | |
| return f"""<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f9fafb;font-size:.82rem"> | |
| <span>{icon}</span><span style="flex:1;color:#374151;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{text}</span> | |
| <span style="font-size:.68rem;color:#9ca3af">{meta}</span> | |
| </div>""" | |
| task_rows = "".join(drow("β¬", t["text"][:45], t.get("due","")) for t in pending[:5]) | |
| ev_rows = "".join(drow("π ", e["title"][:45], e["date"]) for e in up) | |
| doc_rows = "".join(drow(ICONS.get("."+d.get("ftype","").lower(),"π"), d["filename"][:45], d.get("date","")) for d in recent) | |
| return f"""<div style="padding:4px 0"> | |
| <div style="font-size:1.5rem;font-weight:800;color:#111827;letter-spacing:-.02em">{greet}, Manager</div> | |
| <div style="font-size:.80rem;color:#6b7280;margin-bottom:20px">{today_s}</div> | |
| {stats} | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px"> | |
| {card("π Active Tasks", task_rows, "All tasks complete! π")} | |
| {card("ποΈ Upcoming Events", ev_rows, "No upcoming events")} | |
| {card("π Recent Documents", doc_rows, "No documents indexed yet")} | |
| </div></div>""" | |
| # ββ CHAT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def do_chat(message, history, focus): | |
| if not message.strip(): return history, "" | |
| ctx = [] | |
| if focus and focus not in ("", "β All Documents β"): | |
| t = get_text(focus) | |
| if t: ctx.append(f"[{focus}]\n{t[:3000]}") | |
| else: | |
| chunks = load_all() | |
| kw = keyword_search(message, chunks) | |
| for fn, v in sorted(kw.items(), key=lambda x:-x[1]["kw"])[:5]: | |
| t = get_text(fn) | |
| if t: ctx.append(f"[{fn}]\n{t[:800]}") | |
| context = "\n\n---\n\n".join(ctx) if ctx else "No documents indexed yet." | |
| try: | |
| ans = do_chat_llm(message, context, [(h[0],h[1]) for h in history[-8:]]) | |
| except Exception as e: | |
| ans = f"β {e}" | |
| return history + [[message, ans]], "" | |
| def do_analyze(filename): | |
| if not filename: return [["","β οΈ Select a document first."]], [] | |
| text = get_text(filename) | |
| if not text: return [[f'β',f"'{filename}' not in index."]], [] | |
| prompt = f"""Analyze this document as an executive assistant. | |
| # {filename} | |
| ## Executive Summary | |
| ## Key People | |
| ## Important Dates | |
| ## Financial Data | |
| ## Decisions & Action Items | |
| ## Risks & Flags | |
| Document:\n{text[:4000]}""" | |
| try: | |
| return [[f"π {filename}", do_generate(prompt)]], [] | |
| except Exception as e: | |
| return [[f"π {filename}", f"β {e}"]], [] | |
| # ββ EMAIL ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def do_email(instructions, doc, tone): | |
| if not instructions.strip(): return "β οΈ Describe the email first." | |
| ctx = "" | |
| if doc and doc not in ("", "β None β"): | |
| t = get_text(doc) | |
| if t: ctx = f"\n\nDocument context ({doc}):\n{t[:2000]}" | |
| tones = {"Formal & Executive":"formal, authoritative", | |
| "Professional & Warm":"professional, warm", | |
| "Concise & Direct":"very concise, direct", | |
| "Diplomatic":"diplomatic, nuanced"} | |
| prompt = f"""Write a complete professional business email. | |
| Tone: {tones.get(tone,"formal")} | |
| Instructions: {instructions}{ctx} | |
| Format: | |
| Subject: [subject] | |
| Dear [Recipient], | |
| [body] | |
| Best regards, | |
| [Manager Name]""" | |
| try: return do_generate(prompt) | |
| except Exception as e: return f"β {e}" | |
| # ββ TASK HANDLERS ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def add_task(txt, due, pri, note): | |
| if not txt.strip(): return tasks_html(), "β οΈ Enter task text", "", "", "medium", "" | |
| t = load_tasks() | |
| t.append({"text":txt.strip(),"due":due.strip(),"priority":pri,"note":note, | |
| "done":False,"created":datetime.date.today().isoformat()}) | |
| save_tasks(t); return tasks_html(), "", "", "", "medium", "" | |
| def toggle_task(idx): | |
| t = load_tasks() | |
| try: | |
| i = int(idx.strip()) | |
| if 0 <= i < len(t): t[i]["done"] = not t[i]["done"] | |
| save_tasks(t) | |
| except: pass | |
| return tasks_html(), "" | |
| def delete_task(idx): | |
| t = load_tasks() | |
| try: | |
| i = int(idx.strip()) | |
| if 0 <= i < len(t): t.pop(i) | |
| save_tasks(t) | |
| except: pass | |
| return tasks_html(), "" | |
| # ββ EVENT HANDLERS βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def add_event(title, date, time, note): | |
| if not title.strip() or not date.strip(): | |
| return events_html(), "β οΈ Title and date required", "", "", "", "" | |
| e = load_events() | |
| e.append({"title":title.strip(),"date":date.strip(),"time":time.strip(),"note":note.strip()}) | |
| save_events(e); return events_html(), "", "", "", "", "" | |
| def delete_event(idx): | |
| e = load_events() | |
| try: | |
| i = int(idx.strip()) | |
| if 0 <= i < len(e): e.pop(i) | |
| save_events(e) | |
| except: pass | |
| return events_html(), "" | |
| # ββ INDEX HANDLERS βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def do_upload(files, progress=gr.Progress()): | |
| if not files: return "β οΈ No files selected.", lib_stats() | |
| results = [] | |
| for i, f in enumerate(files): | |
| progress(i / len(files), desc=f"Indexing {Path(f.name).name}") | |
| good, msg = index_file(f.name) | |
| results.append(f"{'β ' if good else 'β οΈ'} {Path(f.name).name} β {msg}") | |
| return "\n".join(results), lib_stats() | |
| def do_clear(): | |
| shutil.rmtree(INDEX_DIR, ignore_errors=True) | |
| os.makedirs(INDEX_DIR, exist_ok=True) | |
| return "ποΈ Index cleared.", lib_stats() | |
| def do_load(fname): | |
| if not fname: return "*Select a file.*", "" | |
| text = get_text(fname) | |
| if not text: return f"β '{fname}' not found.", "" | |
| for ff in os.listdir(INDEX_DIR): | |
| if not ff.endswith(".pkl"): continue | |
| try: | |
| with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f) | |
| if data["meta"]["filename"] == fname: | |
| m = data["meta"] | |
| return f"**{fname}** Β· {m.get('words',0):,} words Β· {m.get('mb',0)} MB Β· {m.get('date','')}", text | |
| except: pass | |
| return f"**{fname}**", text | |
| # ββ CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); | |
| body, .gradio-container{background:#f0f4f8!important;font-family:'Inter',sans-serif!important;color:#111827!important} | |
| .gradio-container{max-width:100%!important;padding:0!important} | |
| textarea,input[type=text]{background:#fff!important;border:1.5px solid #d1d5db!important;border-radius:8px!important;color:#111827!important;font-family:'Inter',sans-serif!important;font-size:.86rem!important} | |
| textarea:focus,input:focus{border-color:#1d4ed8!important;outline:none!important;box-shadow:0 0 0 3px rgba(29,78,216,.1)!important} | |
| .gr-button{font-family:'Inter',sans-serif!important;font-weight:600!important;border-radius:8px!important;font-size:.83rem!important} | |
| .gr-button.primary{background:#1d4ed8!important;color:#fff!important;border:none!important} | |
| .gr-button.primary:hover{background:#1e40af!important} | |
| .gr-button.secondary{background:#fff!important;color:#374151!important;border:1.5px solid #d1d5db!important} | |
| ::-webkit-scrollbar{width:5px;height:5px} | |
| ::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:3px} | |
| """ | |
| # ββ UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _ok = True | |
| _badge = ai_status() | |
| with gr.Blocks(title="Manager Intelligence Agent", css=CSS) as demo: | |
| HIST = gr.State([]) | |
| gr.HTML(f"""<div style="background:linear-gradient(135deg,#1e3a8a,#1d4ed8);padding:18px 32px 16px;border-bottom:3px solid #d97706"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px"> | |
| <div style="display:flex;align-items:center;gap:14px"> | |
| <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> | |
| <div> | |
| <div style="font-size:1.4rem;font-weight:800;color:#fff">Manager <span style="color:#fbbf24">Intelligence</span> Agent</div> | |
| <div style="font-size:.68rem;color:rgba(255,255,255,.7);text-transform:uppercase;letter-spacing:.1em;margin-top:3px">Executive OS Β· Free AI Β· Hugging Face Spaces</div> | |
| </div> | |
| </div> | |
| <div style="display:flex;gap:7px;flex-wrap:wrap;align-items:center"> | |
| <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">π¦ Llama-3-8B</span> | |
| <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> | |
| <span style="padding:5px 14px;border-radius:50px;font-size:.72rem;font-weight:600;background:rgba(74,222,128,.15);border:1px solid rgba(74,222,128,.5);color:#4ade80">{_badge}</span> | |
| </div> | |
| </div> | |
| </div>""") | |
| with gr.Tabs(): | |
| with gr.Tab("π Dashboard"): | |
| dash = gr.HTML(dashboard_html()) | |
| gr.Button("π Refresh", variant="secondary").click(dashboard_html, outputs=[dash]) | |
| gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8; | |
| border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-top:10px"> | |
| <strong>Getting started:</strong> Go to <strong>Documents</strong> β upload files β click Index. | |
| Then use <strong>Search</strong> or <strong>Chat</strong> to work with your documents. | |
| </div>""") | |
| with gr.Tab("π Search"): | |
| with gr.Row(): | |
| s_q = gr.Textbox(label="Search", placeholder='"Ahmed Al-Rashidi 2023" Β· "Q3 budget" Β· "contract renewal"', lines=1, scale=5) | |
| s_btn = gr.Button("π Search", variant="primary", scale=1) | |
| s_sum = gr.Markdown("*Enter a query and click Search.*") | |
| s_html = gr.HTML("") | |
| s_dd = gr.Dropdown(label="π Select a file to preview", choices=[], value=None) | |
| def do_search_all(q): | |
| html, choices, dd = run_search(q) | |
| return html, f"π Found **{len(choices)}** results for: *{q}*" if choices else "*No results.*", dd | |
| s_btn.click(do_search_all, inputs=[s_q], outputs=[s_html, s_sum, s_dd]) | |
| s_q.submit(do_search_all, inputs=[s_q], outputs=[s_html, s_sum, s_dd]) | |
| with gr.Tab("π¬ Chat & Intelligence"): | |
| with gr.Row(): | |
| with gr.Column(scale=1, min_width=260): | |
| c_focus = gr.Dropdown(label="Focus on file (optional)", | |
| choices=["β All Documents β"] + all_names(), value="β All Documents β") | |
| gr.Button("π Refresh Files", variant="secondary").click( | |
| lambda: gr.Dropdown(choices=["β All Documents β"] + all_names()), outputs=[c_focus]) | |
| gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8; | |
| border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-top:12px;line-height:1.8"> | |
| <strong>Try asking:</strong><br>β’ Find all records for Ahmed Hassan<br> | |
| β’ Summarize the Q3 financial report<br>β’ List all salary changes 2020β2024<br> | |
| β’ Who approved the merger?<br>β’ What contracts expire this year?</div>""") | |
| 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>") | |
| a_file = gr.Dropdown(label="Select document", choices=all_names()) | |
| gr.Button("π", variant="secondary").click(lambda: gr.Dropdown(choices=all_names()), outputs=[a_file]) | |
| a_btn = gr.Button("π Full Analysis", variant="primary") | |
| with gr.Column(scale=3): | |
| chatbot = gr.Chatbot(label="", height=460, show_label=False) | |
| with gr.Row(): | |
| c_in = gr.Textbox(label="", show_label=False, placeholder="Ask anything about your documents...", lines=2, scale=5) | |
| with gr.Column(scale=1, min_width=90): | |
| c_send = gr.Button("Send β", variant="primary") | |
| c_clear = gr.Button("Clear", variant="secondary") | |
| def chat_fn(msg, hist, focus): | |
| new_hist, _ = do_chat(msg, hist, focus) | |
| return new_hist, "", new_hist | |
| c_send.click(chat_fn, inputs=[c_in, HIST, c_focus], outputs=[HIST, c_in, chatbot]) | |
| c_in.submit(chat_fn, inputs=[c_in, HIST, c_focus], outputs=[HIST, c_in, chatbot]) | |
| c_clear.click(lambda: ([], []), outputs=[HIST, chatbot]) | |
| a_btn.click(do_analyze, inputs=[a_file], outputs=[chatbot, HIST]) | |
| with gr.Tab("βοΈ Email Drafts"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| e_tone = gr.Dropdown(label="Tone", | |
| choices=["Formal & Executive","Professional & Warm","Concise & Direct","Diplomatic"], | |
| value="Formal & Executive") | |
| e_doc = gr.Dropdown(label="Reference document (optional)", | |
| choices=["β None β"] + all_names(), value="β None β") | |
| gr.Button("π Refresh", variant="secondary").click( | |
| lambda: gr.Dropdown(choices=["β None β"] + all_names()), outputs=[e_doc]) | |
| with gr.Column(scale=2): | |
| e_inst = gr.Textbox(label="Email instructions", placeholder="e.g. Write email to HR requesting 2 new engineers...", lines=5) | |
| e_btn = gr.Button("βοΈ Draft Email", variant="primary") | |
| e_out = gr.Textbox(label="Email Draft β copy and send", lines=20, max_lines=35) | |
| e_btn.click(do_email, inputs=[e_inst, e_doc, e_tone], outputs=[e_out]) | |
| with gr.Tab("π Tasks & Calendar"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML("<div style='font-size:.95rem;font-weight:700;color:#111827;margin-bottom:10px'>π Task Manager</div>") | |
| with gr.Row(): | |
| t_txt = gr.Textbox(label="Task", placeholder="What needs to be done?", scale=3) | |
| t_due = gr.Textbox(label="Due (YYYY-MM-DD)", placeholder="2025-12-31", scale=2) | |
| with gr.Row(): | |
| t_pri = gr.Dropdown(label="Priority", choices=["high","medium","low"], value="medium", scale=1) | |
| t_note = gr.Textbox(label="Note", scale=2) | |
| with gr.Row(): | |
| t_add = gr.Button("β Add Task", variant="primary") | |
| t_msg = gr.Markdown("") | |
| t_disp = gr.HTML(tasks_html()) | |
| with gr.Row(): | |
| t_idx = gr.Textbox(label="Task #", placeholder="0", scale=1) | |
| gr.Button("β Toggle", variant="secondary", scale=1).click(toggle_task, inputs=[t_idx], outputs=[t_disp, t_msg]) | |
| gr.Button("ποΈ Delete", variant="secondary", scale=1).click(delete_task, inputs=[t_idx], outputs=[t_disp, t_msg]) | |
| with gr.Column(scale=1): | |
| gr.HTML("<div style='font-size:.95rem;font-weight:700;color:#111827;margin-bottom:10px'>ποΈ Calendar & Events</div>") | |
| with gr.Row(): | |
| ev_t = gr.Textbox(label="Event title", scale=3) | |
| ev_d = gr.Textbox(label="Date (YYYY-MM-DD)", scale=2) | |
| with gr.Row(): | |
| ev_time = gr.Textbox(label="Time", placeholder="14:00", scale=1) | |
| ev_note = gr.Textbox(label="Note", scale=2) | |
| with gr.Row(): | |
| ev_add = gr.Button("π Add Event", variant="primary") | |
| ev_msg = gr.Markdown("") | |
| ev_disp = gr.HTML(events_html()) | |
| with gr.Row(): | |
| ev_idx = gr.Textbox(label="Event # to delete", placeholder="0", scale=1) | |
| gr.Button("ποΈ Delete", variant="secondary", scale=2).click(delete_event, inputs=[ev_idx], outputs=[ev_disp, ev_msg]) | |
| t_add.click(add_task, inputs=[t_txt, t_due, t_pri, t_note], outputs=[t_disp, t_msg, t_txt, t_due, t_pri, t_note]) | |
| ev_add.click(add_event, inputs=[ev_t, ev_d, ev_time, ev_note], outputs=[ev_disp, ev_msg, ev_t, ev_d, ev_time, ev_note]) | |
| with gr.Tab("π Documents"): | |
| with gr.Tabs(): | |
| with gr.Tab("β¬οΈ Upload & Index"): | |
| gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8; | |
| border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-bottom:10px"> | |
| <strong>Upload your files.</strong> Supported: PDF, DOCX, XLSX, CSV, PPTX, TXT, EML.<br> | |
| β οΈ HF free tier = storage resets on restart. Re-upload files after restart. | |
| </div>""") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| f_up = gr.File(label="Select files", file_count="multiple", | |
| file_types=[".pdf",".docx",".doc",".xlsx",".xls",".csv",".txt",".pptx",".ppt",".eml",".rtf"]) | |
| f_upbtn = gr.Button("β‘ Index Uploaded Files", variant="primary") | |
| f_clr = gr.Button("ποΈ Clear Index", variant="secondary") | |
| with gr.Column(scale=1): | |
| f_log = gr.Markdown("*Upload files then click Index.*") | |
| f_stats = gr.Markdown(lib_stats()) | |
| f_upbtn.click(do_upload, inputs=[f_up], outputs=[f_log, f_stats]) | |
| f_clr.click(do_clear, outputs=[f_log, f_stats]) | |
| with gr.Tab("π Preview"): | |
| with gr.Row(): | |
| p_sel = gr.Dropdown(label="Select document", choices=all_names(), scale=4) | |
| p_load = gr.Button("π Load", variant="primary", scale=1) | |
| gr.Button("π", variant="secondary", scale=1).click(lambda: gr.Dropdown(choices=all_names()), outputs=[p_sel]) | |
| p_info = gr.Markdown("*Select a file and click Load.*") | |
| p_text = gr.Textbox(label="Document content", lines=28, max_lines=60) | |
| p_load.click(do_load, inputs=[p_sel], outputs=[p_info, p_text]) | |
| with gr.Tab("βοΈ Setup"): | |
| gr.Markdown(f""" | |
| ## Configuration | |
| **AI Backend:** HF Inference API Β· `{HF_MODEL}` Β· **100% Free** | |
| ### Optional: Add HF Token (removes rate limits) | |
| 1. [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) β New token β Role: **Read** | |
| 2. Space β **Settings β Secrets** β add `HF_TOKEN` | |
| 3. Restart Space | |
| **Current status:** {_badge} | |
| ### Supported Files | |
| | Type | Extensions | | |
| |------|-----------| | |
| | Documents | .pdf .docx .doc .rtf | | |
| | Spreadsheets | .xlsx .xls .csv | | |
| | Presentations | .pptx .ppt | | |
| | Text / Email | .txt .eml | | |
| ### Storage Note | |
| Free tier = ephemeral. Files reset on restart. Re-upload or connect HF Dataset for persistence. | |
| """) | |
| gr.HTML("""<div style="background:#fff;border-top:1px solid #e5e7eb;padding:12px 32px;text-align:center;font-size:.70rem;color:#9ca3af"> | |
| <span style="color:#1d4ed8;font-weight:700">Manager Intelligence Agent</span> Β· Free AI Β· Llama-3-8B Β· Hugging Face Spaces | |
| </div>""") | |
| if __name__ == "__main__": | |
| print(f"\n{'='*50}\n Manager Intelligence Agent\n AI: {ai_status()}\n{'='*50}\n") | |
| demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) | |