File size: 8,173 Bytes
81598c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17ae707
81598c5
17ae707
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81598c5
 
 
 
17ae707
 
 
159527f
17ae707
 
 
81598c5
17ae707
 
 
81598c5
 
 
eea09fd
81598c5
 
 
 
 
 
17ae707
 
81598c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17ae707
 
81598c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eea09fd
81598c5
 
530fa5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81598c5
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""

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)