ScottzillaSystems commited on
Commit
fafcc25
·
verified ·
1 Parent(s): 6db3a7e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -0
app.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Conversation Memory System - SQLite-backed conversation tracker with text search and Markdown export.
4
+ Designed for offline use with optional sentence-transformers for semantic search.
5
+ """
6
+
7
+ import sqlite3
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ import gradio as gr
13
+
14
+ DB_PATH = Path("/data/conversations.db")
15
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
16
+
17
+ # Try optional semantic search
18
+ try:
19
+ from sentence_transformers import SentenceTransformer
20
+ import numpy as np
21
+ EMBED_MODEL = SentenceTransformer("all-MiniLM-L6-v2")
22
+ HAS_EMBEDDINGS = True
23
+ except ImportError:
24
+ HAS_EMBEDDINGS = False
25
+
26
+ class ConversationMemory:
27
+ def __init__(self, db_path: str):
28
+ self.db_path = db_path
29
+ self._init_db()
30
+
31
+ def _init_db(self):
32
+ with sqlite3.connect(self.db_path) as conn:
33
+ conn.executescript("""
34
+ CREATE TABLE IF NOT EXISTS threads (
35
+ thread_id TEXT PRIMARY KEY,
36
+ title TEXT,
37
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
38
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
39
+ );
40
+ CREATE TABLE IF NOT EXISTS messages (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ thread_id TEXT NOT NULL,
43
+ role TEXT NOT NULL CHECK(role IN ('user','assistant','system','agent-zero')),
44
+ content TEXT NOT NULL,
45
+ source TEXT DEFAULT 'user',
46
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47
+ FOREIGN KEY (thread_id) REFERENCES threads(thread_id)
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
50
+ CREATE INDEX IF NOT EXISTS idx_threads_updated ON threads(updated_at);
51
+ """)
52
+ if HAS_EMBEDDINGS:
53
+ conn.execute("""
54
+ CREATE TABLE IF NOT EXISTS embeddings (
55
+ message_id INTEGER PRIMARY KEY,
56
+ embedding BLOB,
57
+ FOREIGN KEY (message_id) REFERENCES messages(id)
58
+ )
59
+ """)
60
+ conn.commit()
61
+
62
+ def save_message(self, role: str, content: str, thread_id: str = None, source: str = "user"):
63
+ if thread_id is None:
64
+ thread_id = f"thread-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
65
+ with sqlite3.connect(self.db_path) as conn:
66
+ conn.execute("INSERT OR IGNORE INTO threads (thread_id, title) VALUES (?, ?)",
67
+ (thread_id, thread_id))
68
+ c = conn.execute("INSERT INTO messages (thread_id, role, content, source) VALUES (?,?,?,?)",
69
+ (thread_id, role, content, source))
70
+ conn.execute("UPDATE threads SET updated_at = CURRENT_TIMESTAMP WHERE thread_id = ?", (thread_id,))
71
+ conn.commit()
72
+ msg_id = c.lastrowid
73
+ if HAS_EMBEDDINGS:
74
+ emb = EMBED_MODEL.encode(content[:512])
75
+ conn.execute("INSERT INTO embeddings (message_id, embedding) VALUES (?,?)",
76
+ (msg_id, emb.tobytes()))
77
+ conn.commit()
78
+ return thread_id
79
+
80
+ def search_text(self, query: str, limit: int = 20):
81
+ with sqlite3.connect(self.db_path) as conn:
82
+ return conn.execute(
83
+ "SELECT thread_id, role, content, created_at FROM messages WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?",
84
+ (f"%{query}%", limit)
85
+ ).fetchall()
86
+
87
+ def search_semantic(self, query: str, limit: int = 10):
88
+ if not HAS_EMBEDDINGS:
89
+ return [("error", "system", "Install sentence-transformers for semantic search", "")]
90
+ q_emb = EMBED_MODEL.encode(query)
91
+ results = []
92
+ with sqlite3.connect(self.db_path) as conn:
93
+ rows = conn.execute("""
94
+ SELECT m.thread_id, m.role, m.content, m.created_at, e.embedding
95
+ FROM messages m JOIN embeddings e ON m.id = e.message_id
96
+ ORDER BY m.created_at DESC LIMIT 500
97
+ """).fetchall()
98
+ for row in rows:
99
+ emb = np.frombuffer(row[4], dtype=np.float32)
100
+ score = np.dot(q_emb, emb) / (np.linalg.norm(q_emb) * np.linalg.norm(emb) + 1e-8)
101
+ results.append((score, row[0], row[1], row[2][:500], row[3]))
102
+ results.sort(key=lambda x: x[0], reverse=True)
103
+ return [(r[1], r[2], r[3], r[4]) for r in results[:limit]]
104
+
105
+ def list_threads(self):
106
+ with sqlite3.connect(self.db_path) as conn:
107
+ return conn.execute(
108
+ "SELECT thread_id, title, created_at, updated_at, (SELECT COUNT(*) FROM messages WHERE thread_id = t.thread_id) as msg_count FROM threads t ORDER BY updated_at DESC LIMIT 50"
109
+ ).fetchall()
110
+
111
+ def get_thread(self, thread_id: str):
112
+ with sqlite3.connect(self.db_path) as conn:
113
+ return conn.execute(
114
+ "SELECT role, content, source, created_at FROM messages WHERE thread_id = ? ORDER BY created_at",
115
+ (thread_id,)
116
+ ).fetchall()
117
+
118
+ def export_markdown(self, thread_id: str) -> str:
119
+ msgs = self.get_thread(thread_id)
120
+ md = f"# Thread: {thread_id}\n\n"
121
+ for role, content, source, ts in msgs:
122
+ md += f"## {role} ({source}) - {ts}\n\n{content}\n\n---\n\n"
123
+ return md
124
+
125
+ def export_all(self) -> str:
126
+ threads = self.list_threads()
127
+ md = "# All Conversations\n\n"
128
+ for tid, title, created, updated, count in threads:
129
+ md += f"## {title} ({count} msgs)\n"
130
+ md += f"Created: {created} | Updated: {updated}\n\n"
131
+ for role, content, source, ts in self.get_thread(tid):
132
+ md += f"### {role} ({source}) - {ts}\n\n{content[:500]}...\n\n"
133
+ md += "---\n\n"
134
+ return md
135
+
136
+ # Initialize
137
+ memory = ConversationMemory(str(DB_PATH))
138
+
139
+ # Gradio UI
140
+ with gr.Blocks(title="Conversation Memory", theme=gr.themes.Soft()) as demo:
141
+ gr.Markdown("""# 💾 Conversation Memory System
142
+ **SQLite-backed** | **Text + Semantic Search** | **Markdown Export** | **Fully Offline**
143
+ """)
144
+
145
+ with gr.Tabs():
146
+ with gr.TabItem("💬 Save"):
147
+ role = gr.Dropdown(["user", "assistant", "system", "agent-zero"], label="Role", value="user")
148
+ content = gr.Textbox(label="Message", lines=4, placeholder="Enter conversation message...")
149
+ thread_id = gr.Textbox(label="Thread ID (optional)", placeholder="auto-generated if blank")
150
+ source = gr.Textbox(label="Source", value="user")
151
+ save_btn = gr.Button("Save Message")
152
+ save_status = gr.Textbox(label="Status")
153
+
154
+ def save(role, content, thread_id, source):
155
+ tid = memory.save_message(role, content, thread_id or None, source)
156
+ return f"Saved to thread: {tid}"
157
+
158
+ save_btn.click(save, [role, content, thread_id, source], save_status)
159
+
160
+ with gr.TabItem("🔍 Search"):
161
+ search_query = gr.Textbox(label="Search Query")
162
+ search_mode = gr.Radio(["Text", "Semantic"], label="Mode", value="Text")
163
+ search_btn = gr.Button("Search")
164
+ search_results = gr.Dataframe(
165
+ headers=["Thread", "Role", "Content", "Time"],
166
+ label="Results"
167
+ )
168
+
169
+ def do_search(query, mode):
170
+ if mode == "Text":
171
+ results = memory.search_text(query)
172
+ else:
173
+ results = memory.search_semantic(query)
174
+ return [[r[0], r[1], r[2][:300], r[3]] for r in results]
175
+
176
+ search_btn.click(do_search, [search_query, search_mode], search_results)
177
+
178
+ with gr.TabItem("📋 Threads"):
179
+ threads_list = gr.Dataframe(
180
+ headers=["Thread ID", "Title", "Created", "Updated", "Messages"],
181
+ label="All Threads"
182
+ )
183
+ refresh_btn = gr.Button("Refresh")
184
+ refresh_btn.click(lambda: memory.list_threads(), None, threads_list)
185
+
186
+ thread_detail_id = gr.Textbox(label="Thread ID")
187
+ show_thread_btn = gr.Button("Show Thread")
188
+ thread_content = gr.Markdown(label="Thread Content")
189
+
190
+ def show_thread(tid):
191
+ if not tid: return "Enter a thread ID"
192
+ msgs = memory.get_thread(tid)
193
+ return "\n\n".join(f"**{r[0]}** ({r[2]}) - {r[3]}\n\n{r[1]}" for r in msgs)
194
+
195
+ show_thread_btn.click(show_thread, thread_detail_id, thread_content)
196
+
197
+ with gr.TabItem("📤 Export"):
198
+ export_thread_id = gr.Textbox(label="Thread ID (or 'all' for everything)")
199
+ export_btn = gr.Button("Export to Markdown")
200
+ export_output = gr.Markdown(label="Exported Markdown")
201
+
202
+ def do_export(tid):
203
+ if tid == "all":
204
+ return memory.export_all()
205
+ return memory.export_markdown(tid)
206
+
207
+ export_btn.click(do_export, export_thread_id, export_output)
208
+
209
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860)