-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathui.py
More file actions
252 lines (210 loc) · 9.34 KB
/
Copy pathui.py
File metadata and controls
252 lines (210 loc) · 9.34 KB
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
"""
cortex/ui.py
------------
Streamlit chat UI for Cortex — your universal second brain.
Run: streamlit run ui.py
"""
import os
import streamlit as st
from datetime import datetime
from dotenv import load_dotenv
load_dotenv()
from instrumentation import setup_tracing
setup_tracing(project_name="cortex")
from anthropic import Anthropic
from redis_store import search_context, get_recent_context
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
# ── Page config ───────────────────────────────────────────────────────────────
st.set_page_config(
page_title="Cortex",
page_icon="🧠",
layout="wide",
initial_sidebar_state="expanded",
)
# ── Styles ────────────────────────────────────────────────────────────────────
st.markdown("""
<style>
.cortex-header { font-size: 2rem; font-weight: 700; margin-bottom: 0; }
.cortex-sub { color: #888; font-size: 0.95rem; margin-top: 0; margin-bottom: 2rem; }
.source-card {
background: #1e1e2e;
border: 1px solid #313147;
border-radius: 8px;
padding: 10px 14px;
margin: 6px 0;
font-size: 0.85rem;
}
.source-tag {
display: inline-block;
background: #313147;
border-radius: 4px;
padding: 2px 8px;
font-size: 0.75rem;
margin-right: 6px;
color: #a0a0c0;
}
.source-type {
color: #7c7cff;
font-weight: 600;
}
.recent-item {
border-left: 2px solid #7c7cff;
padding-left: 10px;
margin: 8px 0;
font-size: 0.82rem;
color: #ccc;
}
</style>
""", unsafe_allow_html=True)
# ── Source emoji map ──────────────────────────────────────────────────────────
SOURCE_EMOJI = {
"claude": "⚡",
"claude-chat": "⚡",
"chatgpt": "🤖",
"gemini": "✨",
"slack": "💬",
"imessage": "💬",
"notion": "📝",
"chrome": "🌐",
"safari": "🧭",
"arc": "🌐",
"omi": "🎙️",
"email": "📧",
"apple-notes": "📓",
"unknown": "📌",
}
TYPE_COLOR = {
"insight": "#7c7cff",
"decision": "#ff7c7c",
"open-question": "#ffb07c",
"action": "#7cffb0",
"summary": "#c0c0c0",
"person": "#ff7ce0",
}
def source_emoji(source: str) -> str:
for key, emoji in SOURCE_EMOJI.items():
if key in source.lower():
return emoji
return "📌"
def type_color(note_type: str) -> str:
return TYPE_COLOR.get(note_type, "#888")
# ── Answer with context ───────────────────────────────────────────────────────
def ask_cortex(question: str, context_chunks: list[dict]) -> str:
"""Ask Claude to answer using retrieved Cortex context."""
if not context_chunks:
return "I couldn't find anything relevant in your Cortex. Try capturing more context first using Cmd+C → Cmd+Shift+V."
# Format context for Claude
context_text = "\n\n".join([
f"[{c.get('type', 'note')} from {c.get('source', '?')} on {c.get('timestamp', '')[:10]}]\n{c.get('content', '')}"
for c in context_chunks
])
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1000,
system="""You are Cortex — a personal AI that knows everything about the user based on their captured context.
You have access to the user's second brain: notes, decisions, insights, and memories captured from their AI chats, Slack, iMessage, and other apps.
Answer questions directly and personally, as if you are their most knowledgeable assistant.
- Reference specific details from the context (dates, sources, exact decisions)
- Be concise but complete
- If the context is partial, say so and answer with what you have
- Never say "based on the provided context" — just answer naturally""",
messages=[{
"role": "user",
"content": f"Context from my second brain:\n\n{context_text}\n\n---\n\nQuestion: {question}"
}]
)
return response.content[0].text
# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
st.markdown("### 🧠 Cortex")
st.markdown("*Your universal second brain*")
st.divider()
# Recent captures
st.markdown("**Recent captures**")
try:
recent = get_recent_context(since=datetime.now().replace(hour=0, minute=0, second=0), top_k=10)
if recent:
for item in recent[:8]:
ts = item.get("timestamp", "")[:16].replace("T", " ")
src = source_emoji(item.get("source", ""))
content_preview = item.get("content", "")[:80]
st.markdown(f"""<div class="recent-item">{src} <b>{ts}</b><br>{content_preview}...</div>""", unsafe_allow_html=True)
else:
st.caption("No captures today yet. Select text anywhere and press Cmd+C → Cmd+Shift+V.")
except Exception:
st.caption("Redis not connected — start with: docker start cortex-redis")
st.divider()
# Stats
repo = os.environ.get("CORTEX_REPO", "")
if repo:
st.markdown(f"**[📁 GitHub repo](https://github.com/{repo})**")
st.markdown("**Hotkey:** Select text → `Cmd+C` → `Cmd+Shift+V`")
# Top K slider
st.divider()
top_k = st.slider("Context depth", min_value=3, max_value=15, value=6,
help="How many memory chunks to retrieve per question")
# ── Main chat area ────────────────────────────────────────────────────────────
st.markdown('<p class="cortex-header">🧠 Cortex</p>', unsafe_allow_html=True)
st.markdown('<p class="cortex-sub">Ask anything about your life, work, decisions, and conversations.</p>', unsafe_allow_html=True)
# Init chat history
if "messages" not in st.session_state:
st.session_state.messages = []
# Render chat history
for msg in st.session_state.messages:
with st.chat_message(msg["role"], avatar="🧠" if msg["role"] == "assistant" else "👤"):
st.markdown(msg["content"])
# Show sources if available
if msg.get("sources"):
with st.expander(f"📎 {len(msg['sources'])} sources", expanded=False):
for src in msg["sources"]:
note_type = src.get("type", "note")
source = src.get("source", "?")
ts = src.get("timestamp", "")[:10]
content = src.get("content", "")[:150]
color = type_color(note_type)
emoji = source_emoji(source)
st.markdown(f"""<div class="source-card">
<span class="source-type" style="color:{color}">● {note_type}</span>
<span class="source-tag">{emoji} {source}</span>
<span class="source-tag">📅 {ts}</span>
<br><br>{content}{"..." if len(src.get("content","")) > 150 else ""}
</div>""", unsafe_allow_html=True)
# Chat input
if question := st.chat_input("What do you know about..."):
# Add user message
st.session_state.messages.append({"role": "user", "content": question})
with st.chat_message("user", avatar="👤"):
st.markdown(question)
# Search Cortex + generate answer
with st.chat_message("assistant", avatar="🧠"):
with st.spinner("Searching your second brain..."):
try:
context_chunks = search_context(question, top_k=top_k)
except Exception as e:
context_chunks = []
st.warning(f"Redis search failed: {e}")
with st.spinner("Synthesizing answer..."):
answer = ask_cortex(question, context_chunks)
st.markdown(answer)
# Show sources
if context_chunks:
with st.expander(f"📎 {len(context_chunks)} sources", expanded=False):
for src in context_chunks:
note_type = src.get("type", "note")
source = src.get("source", "?")
ts = src.get("timestamp", "")[:10]
content = src.get("content", "")[:150]
color = type_color(note_type)
emoji = source_emoji(source)
st.markdown(f"""<div class="source-card">
<span class="source-type" style="color:{color}">● {note_type}</span>
<span class="source-tag">{emoji} {source}</span>
<span class="source-tag">📅 {ts}</span>
<br><br>{content}{"..." if len(src.get("content","")) > 150 else ""}
</div>""", unsafe_allow_html=True)
# Save to history
st.session_state.messages.append({
"role": "assistant",
"content": answer,
"sources": context_chunks,
})