Spaces:
Sleeping
Sleeping
| """ | |
| OpenMark Gradio UI β 3 tabs: | |
| 1. Chat β talk to the LangGraph agent | |
| 2. Search β instant semantic search with filters | |
| 3. Stats β knowledge base overview | |
| """ | |
| import sys | |
| import os | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| import gradio as gr | |
| from openmark import config | |
| # Load once at startup β fail gracefully if credentials are not configured | |
| print("Loading OpenMark...") | |
| _embedder = None | |
| _agent = None | |
| _setup_error = None | |
| try: | |
| from openmark.embeddings.factory import get_embedder | |
| from openmark.agent.graph import build_agent, ask | |
| from openmark.stores import chroma as chroma_store | |
| from openmark.stores import neo4j_store | |
| _embedder = get_embedder() | |
| _agent = build_agent() | |
| print("OpenMark ready.") | |
| except Exception as e: | |
| _setup_error = str(e) | |
| print(f"OpenMark setup incomplete: {e}") | |
| # ββ Chat tab ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _NOT_READY = ( | |
| "## Setup required\n\n" | |
| "This Space is a **demo shell** β it requires your own credentials to run.\n\n" | |
| "See the [GitHub repo](https://github.com/OthmanAdi/OpenMark) for full setup instructions." | |
| ) | |
| def chat_fn(message: str, history: list, thread_id: str): | |
| if _agent is None: | |
| history.append((message, _NOT_READY)) | |
| return history, "" | |
| if not message.strip(): | |
| return history, "" | |
| response = ask(_agent, message, thread_id=thread_id or "default") | |
| history.append((message, response)) | |
| return history, "" | |
| # ββ Search tab ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def search_fn(query: str, category: str, min_score: float, n_results: int): | |
| if _embedder is None: | |
| return _NOT_READY | |
| if not query.strip(): | |
| return "Enter a search query." | |
| cat = category if category != "All" else None | |
| ms = min_score if min_score > 0 else None | |
| results = chroma_store.search( | |
| query, _embedder, n=int(n_results), | |
| category=cat, min_score=ms, | |
| ) | |
| if not results: | |
| return "No results found." | |
| lines = [] | |
| for r in results: | |
| lines.append( | |
| f"**{r['rank']}. {r['title'] or r['url']}**\n" | |
| f"π {r['url']}\n" | |
| f"π {r['category']} | π {', '.join(t for t in r['tags'] if t)} | " | |
| f"β {r['score']} | π― {r['similarity']:.3f} similarity\n" | |
| ) | |
| return "\n---\n".join(lines) | |
| # ββ Stats tab βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def stats_fn(): | |
| if _embedder is None: | |
| return _NOT_READY | |
| chroma = chroma_store.get_stats() | |
| neo4j = neo4j_store.get_stats() | |
| # Category breakdown from Neo4j | |
| cat_rows = neo4j_store.query(""" | |
| MATCH (b:Bookmark)-[:IN_CATEGORY]->(c:Category) | |
| RETURN c.name AS category, count(b) AS count | |
| ORDER BY count DESC | |
| """) | |
| cat_lines = "\n".join(f" {r['category']:<35} {r['count']:>5}" for r in cat_rows) | |
| # Top tags | |
| tag_rows = neo4j_store.query(""" | |
| MATCH (b:Bookmark)-[:TAGGED]->(t:Tag) | |
| RETURN t.name AS tag, count(b) AS count | |
| ORDER BY count DESC LIMIT 20 | |
| """) | |
| tag_lines = ", ".join(f"{r['tag']} ({r['count']})" for r in tag_rows) | |
| return ( | |
| f"## OpenMark Knowledge Base\n\n" | |
| f"**ChromaDB vectors:** {chroma.get('total', 0)}\n" | |
| f"**Neo4j bookmarks:** {neo4j.get('bookmarks', 0)}\n" | |
| f"**Neo4j tags:** {neo4j.get('tags', 0)}\n" | |
| f"**Neo4j categories:** {neo4j.get('categories', 0)}\n\n" | |
| f"### By Category\n```\n{cat_lines}\n```\n\n" | |
| f"### Top Tags\n{tag_lines}" | |
| ) | |
| # ββ Build UI ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_ui(): | |
| with gr.Blocks(title="OpenMark") as app: | |
| gr.Markdown("# OpenMark β Your Personal Knowledge Graph") | |
| if _setup_error: | |
| # No credentials β show landing page only | |
| gr.Markdown( | |
| "**8,000+ bookmarks, LinkedIn saves, and YouTube videos** indexed with " | |
| "[pplx-embed](https://huggingface.co/collections/perplexity-ai/pplx-embed), " | |
| "searchable with ChromaDB and Neo4j, queryable via a LangGraph agent.\n\n" | |
| "Built by [Ahmad Othman Ammar Adi](https://github.com/OthmanAdi) Β· " | |
| "[GitHub](https://github.com/OthmanAdi/OpenMark) Β· " | |
| "[Dataset](https://huggingface.co/datasets/codingwithadi/openmark-bookmarks)\n\n" | |
| "---\n\n" | |
| "## Run it yourself\n\n" | |
| "This Space requires your own data and credentials. " | |
| "Clone the repo and follow the setup guide:\n\n" | |
| "```bash\n" | |
| "git clone https://github.com/OthmanAdi/OpenMark.git\n" | |
| "cd OpenMark\n" | |
| "pip install -r requirements.txt\n" | |
| "cp .env.example .env # add your keys\n" | |
| "python scripts/ingest.py\n" | |
| "python openmark/ui/app.py\n" | |
| "```" | |
| ) | |
| else: | |
| categories = ["All"] + config.CATEGORIES | |
| with gr.Tabs(): | |
| # Tab 1: Chat | |
| with gr.Tab("Chat"): | |
| thread = gr.Textbox(value="default", label="Session ID", scale=1) | |
| chatbot = gr.Chatbot(height=500) | |
| msg_box = gr.Textbox( | |
| placeholder="Ask anything about your saved bookmarks...", | |
| label="Message", lines=2, | |
| ) | |
| send_btn = gr.Button("Send", variant="primary") | |
| send_btn.click( | |
| chat_fn, | |
| inputs=[msg_box, chatbot, thread], | |
| outputs=[chatbot, msg_box], | |
| ) | |
| msg_box.submit( | |
| chat_fn, | |
| inputs=[msg_box, chatbot, thread], | |
| outputs=[chatbot, msg_box], | |
| ) | |
| # Tab 2: Search | |
| with gr.Tab("Search"): | |
| with gr.Row(): | |
| q_input = gr.Textbox(placeholder="Search your knowledge base...", label="Query", scale=3) | |
| cat_input = gr.Dropdown(categories, value="All", label="Category") | |
| with gr.Row(): | |
| score_input = gr.Slider(0, 10, value=0, step=1, label="Min Quality Score") | |
| n_input = gr.Slider(5, 50, value=10, step=5, label="Results") | |
| search_btn = gr.Button("Search", variant="primary") | |
| search_output = gr.Markdown() | |
| search_btn.click( | |
| search_fn, | |
| inputs=[q_input, cat_input, score_input, n_input], | |
| outputs=search_output, | |
| ) | |
| q_input.submit( | |
| search_fn, | |
| inputs=[q_input, cat_input, score_input, n_input], | |
| outputs=search_output, | |
| ) | |
| # Tab 3: Stats | |
| with gr.Tab("Stats"): | |
| refresh_btn = gr.Button("Refresh Stats") | |
| stats_output = gr.Markdown() | |
| refresh_btn.click(stats_fn, outputs=stats_output) | |
| app.load(stats_fn, outputs=stats_output) | |
| return app | |
| if __name__ == "__main__": | |
| ui = build_ui() | |
| ui.launch(server_name="0.0.0.0", server_port=7860, share=False) | |