wolfgangspoony Claude Opus 4.5 commited on
Commit
8299734
Β·
1 Parent(s): 583e8f8

Restore ReActAgent architecture that was gutted in a061524

Browse files

The "Clean DeepSeek integration" commit broke the entire agent by
replacing the proper LlamaIndex ReActAgent with a manual SEARCH: parsing
hack that doesn't work.

This restores:
- ReActAgent with automatic tool use
- The full system prompt with axioms and operational rules
- Proper tool description explaining the knowledge base contents
- Memory context injection
- Reasoning trace capture

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +80 -206
  2. knowledge.py +203 -419
  3. requirements.txt +1 -1
app.py CHANGED
@@ -2,74 +2,28 @@ import gradio as gr
2
  import json
3
  import shutil
4
  import tempfile
5
- import time
6
- import uuid
7
- from datetime import datetime, timedelta
8
  from pathlib import Path
9
  import knowledge
10
  from knowledge import (
11
- run_agent,
 
 
 
12
  load_memory,
13
  reload_memory_sync,
14
- get_or_build_index,
15
- SearchTool,
16
  MEMORY_FILE,
17
- init,
18
- init_async,
19
- MAX_MESSAGE_LENGTH,
20
  )
21
 
22
  PIN = "1969"
23
 
24
- # Initialize on startup (sync version for module load)
25
- print("Initializing Brain...")
26
- index, shared_memory = init()
27
- search_tool = SearchTool(index, top_k=10) if index else None
28
- print(f"API Key configured: {bool(knowledge.DEEPSEEK_API_KEY)}")
29
- print(f"Index loaded: {bool(index)}")
30
-
31
-
32
- class RateLimiter:
33
- """Rate limiter with automatic cleanup of old entries."""
34
-
35
- def __init__(self, min_interval: float = 1.0, cleanup_interval: int = 100):
36
- self.min_interval = min_interval
37
- self._requests = {}
38
- self._cleanup_counter = 0
39
- self._cleanup_interval = cleanup_interval
40
-
41
- def is_allowed(self, session_id: str) -> bool:
42
- """Check if request is allowed and record it."""
43
- if not session_id:
44
- return True # Allow if no session (shouldn't happen)
45
-
46
- now = time.time()
47
-
48
- # Periodic cleanup
49
- self._cleanup_counter += 1
50
- if self._cleanup_counter >= self._cleanup_interval:
51
- self._cleanup(now)
52
- self._cleanup_counter = 0
53
-
54
- # Check rate limit
55
- if session_id in self._requests:
56
- elapsed = now - self._requests[session_id]
57
- if elapsed < self.min_interval:
58
- return False
59
-
60
- self._requests[session_id] = now
61
- return True
62
-
63
- def _cleanup(self, now: float):
64
- """Remove entries older than 5 minutes."""
65
- cutoff = now - 300 # 5 minutes
66
- old_keys = [k for k, v in self._requests.items() if v < cutoff]
67
- for k in old_keys:
68
- del self._requests[k]
69
-
70
-
71
- # Global rate limiter
72
- rate_limiter = RateLimiter(min_interval=1.0)
73
 
74
 
75
  def check_pin(pin_input):
@@ -81,14 +35,14 @@ def check_pin(pin_input):
81
  return gr.update(visible=False), gr.update(visible=True), "Wrong PIN"
82
 
83
 
84
- async def load_history():
85
- """Load conversation history for display."""
86
  try:
87
- mem = await load_memory()
88
  sessions = mem.get("sessions", [])
89
  if not sessions:
90
  return "No conversation history yet."
91
-
92
  output = []
93
  for i, ex in enumerate(reversed(sessions[-50:])):
94
  output.append(f"[{len(sessions)-i}] {ex.get('timestamp', '?')}")
@@ -108,170 +62,100 @@ def export_memory():
108
  """Export memory.json for download."""
109
  if not MEMORY_FILE.exists():
110
  return None
111
-
112
- # Copy to temp file for download
113
  temp_dir = tempfile.mkdtemp()
114
  temp_path = Path(temp_dir) / "brain_memory.json"
115
  shutil.copy(MEMORY_FILE, temp_path)
116
-
117
  return temp_path
118
 
119
 
120
  def import_memory(file):
121
  """Import memory.json from upload."""
122
- global shared_memory
123
-
124
  if file is None:
125
  return "No file uploaded"
126
-
127
  try:
128
  uploaded_path = Path(file.name)
129
- # Validate JSON
130
  with open(uploaded_path, 'r', encoding='utf-8') as f:
131
  data = json.load(f)
132
-
133
  if "sessions" not in data:
134
  return "Invalid memory file: missing 'sessions'"
135
-
136
- # Backup current memory
137
  if MEMORY_FILE.exists():
138
  backup_path = MEMORY_FILE.with_suffix('.backup.json')
139
  shutil.copy(MEMORY_FILE, backup_path)
140
-
141
- # Copy uploaded file to memory location
142
  shutil.copy(uploaded_path, MEMORY_FILE)
143
-
144
- # Reload memory from disk
145
- shared_memory = reload_memory_sync()
146
-
147
- session_count = len(shared_memory.get("sessions", []))
148
- return f"βœ… Memory imported successfully! {session_count} sessions loaded."
 
 
149
  except Exception as e:
150
- return f"❌ Error importing: {str(e)}"
151
 
152
 
153
- def generate_session_id():
154
- """Generate unique session ID."""
155
- return str(uuid.uuid4())
156
 
 
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  with gr.Blocks(title="Brain") as demo:
159
- gr.Markdown("# 🧠 Brain")
160
-
161
- # Session state - initialized once per browser session
162
- session_id = gr.State(generate_session_id)
163
-
164
  with gr.Tabs():
165
  with gr.TabItem("Chat"):
166
- chatbot = gr.Chatbot(
167
- label="Brain",
168
- height=500,
169
- value=[],
170
  )
171
- msg = gr.Textbox(
172
- placeholder="Type here...",
173
- show_label=False,
174
- max_lines=10,
175
- )
176
-
177
- with gr.Row():
178
- send_btn = gr.Button("Send", variant="primary")
179
- clear_btn = gr.Button("Clear Chat")
180
-
181
- # Status indicator
182
- status_msg = "βœ… System ready"
183
- if not knowledge.DEEPSEEK_API_KEY:
184
- status_msg = "❌ Error: DEEPSEEK_API_KEY not configured"
185
- elif not index:
186
- status_msg = "⚠️ Warning: Knowledge index not loaded"
187
-
188
- status_text = gr.Markdown(status_msg)
189
-
190
- async def respond(message, history, session):
191
- # Gradio 6.x uses list of dicts format for Chatbot
192
- if history is None:
193
- history = []
194
- elif not isinstance(history, list):
195
- history = []
196
-
197
- try:
198
- # Input validation
199
- if not message or not message.strip():
200
- return "", history, session
201
-
202
- display_message = message[:500] if len(message) > 500 else message
203
-
204
- # Rate limiting
205
- if not rate_limiter.is_allowed(session):
206
- new_history = history + [
207
- {"role": "user", "content": [{"type": "text", "text": display_message}]},
208
- {"role": "assistant", "content": [{"type": "text", "text": "⚠️ Please wait a moment before sending another message."}]}
209
- ]
210
- return "", new_history, session
211
-
212
- # Check system ready
213
- if not knowledge.DEEPSEEK_API_KEY:
214
- new_history = history + [
215
- {"role": "user", "content": [{"type": "text", "text": display_message}]},
216
- {"role": "assistant", "content": [{"type": "text", "text": "❌ Error: DEEPSEEK_API_KEY not configured."}]}
217
- ]
218
- return "", new_history, session
219
-
220
- if not search_tool:
221
- new_history = history + [
222
- {"role": "user", "content": [{"type": "text", "text": display_message}]},
223
- {"role": "assistant", "content": [{"type": "text", "text": "❌ Error: Knowledge index not available."}]}
224
- ]
225
- return "", new_history, session
226
-
227
- response, searches = await run_agent(
228
- search_tool, message, shared_memory, knowledge.DEEPSEEK_API_KEY
229
- )
230
-
231
- # Format response
232
- display_response = response
233
- if searches:
234
- search_info = f"πŸ” {', '.join(searches)}\n\n"
235
- display_response = search_info + response
236
-
237
- # Gradio 6.x format: list of dicts with role and content array
238
- new_history = history + [
239
- {"role": "user", "content": [{"type": "text", "text": display_message}]},
240
- {"role": "assistant", "content": [{"type": "text", "text": display_response}]}
241
- ]
242
- return "", new_history, session
243
-
244
- except Exception as e:
245
- import traceback
246
- error_detail = traceback.format_exc()
247
- print(f"ERROR in respond: {error_detail}")
248
- new_history = history + [
249
- {"role": "user", "content": [{"type": "text", "text": str(message)[:500]}]},
250
- {"role": "assistant", "content": [{"type": "text", "text": f"❌ Error: {str(e)}"}]}
251
- ]
252
- return "", new_history, session
253
-
254
- def clear_chat():
255
- return [] # Return empty list for Gradio 6.x Chatbot
256
-
257
- send_btn.click(
258
- respond,
259
- [msg, chatbot, session_id],
260
- [msg, chatbot, session_id]
261
- )
262
- msg.submit(
263
- respond,
264
- [msg, chatbot, session_id],
265
- [msg, chatbot, session_id]
266
- )
267
- clear_btn.click(clear_chat, outputs=[chatbot])
268
 
269
  with gr.TabItem("History"):
270
  gr.Markdown("View conversation history (PIN protected)")
271
-
272
  with gr.Column(visible=True) as pin_section:
273
  pin_input = gr.Textbox(
274
- label="PIN",
275
  type="password",
276
  max_lines=1,
277
  )
@@ -293,7 +177,6 @@ with gr.Blocks(title="Brain") as demo:
293
  max_lines=50,
294
  interactive=False,
295
  )
296
- # File component for download
297
  export_file = gr.File(label="Download Memory")
298
 
299
  def lock_history():
@@ -304,26 +187,17 @@ with gr.Blocks(title="Brain") as demo:
304
  inputs=[pin_input],
305
  outputs=[history_section, pin_section, pin_error]
306
  )
307
- refresh_btn.click(fn=load_history, outputs=[history_display])
308
- pin_btn.click(fn=load_history, outputs=[history_display])
309
  lock_btn.click(
310
  fn=lock_history,
311
  outputs=[history_section, pin_section, pin_error]
312
  )
313
  export_btn.click(fn=export_memory, outputs=[export_file])
314
-
315
- # Import wiring
316
  import_btn.click(lambda: gr.update(visible=True), outputs=[import_file])
317
  import_file.change(fn=import_memory, inputs=[import_file], outputs=[import_status])
318
-
319
- # Clear PIN input after attempt
320
  pin_btn.click(lambda: "", outputs=[pin_input])
321
 
322
  if __name__ == "__main__":
323
- demo.queue(
324
- default_concurrency_limit=5,
325
- api_open=False, # Disable API access
326
- )
327
- demo.launch(
328
- show_error=True,
329
- )
 
2
  import json
3
  import shutil
4
  import tempfile
 
 
 
5
  from pathlib import Path
6
  import knowledge
7
  from knowledge import (
8
+ agent,
9
+ agent_ctx,
10
+ memory,
11
+ run_with_trace,
12
  load_memory,
13
  reload_memory_sync,
 
 
14
  MEMORY_FILE,
 
 
 
15
  )
16
 
17
  PIN = "1969"
18
 
19
+
20
+ def get_text(content):
21
+ """Extract plain text from Gradio 6 content blocks."""
22
+ if isinstance(content, str):
23
+ return content
24
+ if isinstance(content, list):
25
+ return " ".join(b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text")
26
+ return str(content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
 
29
  def check_pin(pin_input):
 
35
  return gr.update(visible=False), gr.update(visible=True), "Wrong PIN"
36
 
37
 
38
+ def format_history():
39
+ """Load and format conversation history for display."""
40
  try:
41
+ mem = load_memory()
42
  sessions = mem.get("sessions", [])
43
  if not sessions:
44
  return "No conversation history yet."
45
+
46
  output = []
47
  for i, ex in enumerate(reversed(sessions[-50:])):
48
  output.append(f"[{len(sessions)-i}] {ex.get('timestamp', '?')}")
 
62
  """Export memory.json for download."""
63
  if not MEMORY_FILE.exists():
64
  return None
 
 
65
  temp_dir = tempfile.mkdtemp()
66
  temp_path = Path(temp_dir) / "brain_memory.json"
67
  shutil.copy(MEMORY_FILE, temp_path)
 
68
  return temp_path
69
 
70
 
71
  def import_memory(file):
72
  """Import memory.json from upload."""
 
 
73
  if file is None:
74
  return "No file uploaded"
75
+
76
  try:
77
  uploaded_path = Path(file.name)
 
78
  with open(uploaded_path, 'r', encoding='utf-8') as f:
79
  data = json.load(f)
80
+
81
  if "sessions" not in data:
82
  return "Invalid memory file: missing 'sessions'"
83
+
 
84
  if MEMORY_FILE.exists():
85
  backup_path = MEMORY_FILE.with_suffix('.backup.json')
86
  shutil.copy(MEMORY_FILE, backup_path)
87
+
 
88
  shutil.copy(uploaded_path, MEMORY_FILE)
89
+
90
+ # Reload memory
91
+ global memory
92
+ memory = reload_memory_sync()
93
+ knowledge.memory = memory
94
+
95
+ session_count = len(memory.get("sessions", []))
96
+ return f"Memory imported successfully! {session_count} sessions loaded."
97
  except Exception as e:
98
+ return f"Error importing: {str(e)}"
99
 
100
 
101
+ async def chat(message, history):
102
+ """Main chat handler."""
103
+ user_text = get_text(message)
104
 
105
+ if not user_text or not user_text.strip():
106
+ return ""
107
 
108
+ if agent is None or agent_ctx is None:
109
+ return "Error: System failed to initialize. Check that DEEPSEEK_API_KEY is set."
110
+
111
+ try:
112
+ reasoning, response, searches = await run_with_trace(
113
+ agent, agent_ctx, user_text, knowledge.memory
114
+ )
115
+
116
+ # Build output with reasoning trace
117
+ output = ""
118
+
119
+ if searches:
120
+ output += "**Searches:**\n"
121
+ for s in searches:
122
+ output += f"- `{s}`\n"
123
+ output += "\n"
124
+
125
+ if reasoning:
126
+ output += "**Reasoning:**\n"
127
+ for step in reasoning:
128
+ if step["type"] == "tool_call":
129
+ output += f"> **Tool:** {step['tool']}\n"
130
+ output += f"> **Found:** {step['result_preview']}\n\n"
131
+ output += "---\n\n"
132
+
133
+ output += response
134
+ return output
135
+
136
+ except Exception as e:
137
+ import traceback
138
+ traceback.print_exc()
139
+ return f"Error: {str(e)}"
140
+
141
+
142
+ # Build UI
143
  with gr.Blocks(title="Brain") as demo:
144
+ gr.Markdown("# Brain")
145
+
 
 
 
146
  with gr.Tabs():
147
  with gr.TabItem("Chat"):
148
+ chatbot = gr.ChatInterface(
149
+ fn=chat,
150
+ chatbot=gr.Chatbot(height=500),
 
151
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  with gr.TabItem("History"):
154
  gr.Markdown("View conversation history (PIN protected)")
155
+
156
  with gr.Column(visible=True) as pin_section:
157
  pin_input = gr.Textbox(
158
+ label="PIN",
159
  type="password",
160
  max_lines=1,
161
  )
 
177
  max_lines=50,
178
  interactive=False,
179
  )
 
180
  export_file = gr.File(label="Download Memory")
181
 
182
  def lock_history():
 
187
  inputs=[pin_input],
188
  outputs=[history_section, pin_section, pin_error]
189
  )
190
+ refresh_btn.click(fn=format_history, outputs=[history_display])
191
+ pin_btn.click(fn=format_history, outputs=[history_display])
192
  lock_btn.click(
193
  fn=lock_history,
194
  outputs=[history_section, pin_section, pin_error]
195
  )
196
  export_btn.click(fn=export_memory, outputs=[export_file])
 
 
197
  import_btn.click(lambda: gr.update(visible=True), outputs=[import_file])
198
  import_file.change(fn=import_memory, inputs=[import_file], outputs=[import_status])
 
 
199
  pin_btn.click(lambda: "", outputs=[pin_input])
200
 
201
  if __name__ == "__main__":
202
+ demo.queue(default_concurrency_limit=5)
203
+ demo.launch(show_error=True)
 
 
 
 
 
knowledge.py CHANGED
@@ -1,136 +1,181 @@
1
  """
2
- Brain - Clean DeepSeek Integration
 
3
  """
4
  import os
5
  import json
6
- import asyncio
7
- import uuid
8
  from pathlib import Path
9
  from datetime import datetime
10
- from typing import List, Dict, Tuple
11
- import httpx
12
 
13
  from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
 
 
 
 
 
14
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
15
  from llama_index.vector_stores.chroma import ChromaVectorStore
16
- from llama_index.core.storage import StorageContext
17
  import chromadb
18
 
19
- # Paths
20
  KNOWLEDGE_DIR = Path(__file__).parent / "KNOWLEDGE"
21
  MEMORY_FILE = Path(__file__).parent / "memory.json"
22
  CHROMA_DIR = Path(__file__).parent / ".chroma"
23
 
24
  # HF Spaces persistent storage - always use /data on HF Spaces
25
  if os.environ.get("SPACE_ID"):
26
- # On HF Spaces, /data is the persistent directory
27
  persistent = Path("/data")
28
  persistent.mkdir(parents=True, exist_ok=True)
29
  MEMORY_FILE = persistent / "memory.json"
30
  CHROMA_DIR = persistent / ".chroma"
31
 
32
- DEEPSEEK_API_KEY = ""
33
- _search_index = None
34
- _memory_cache = None
35
- _memory_lock = asyncio.Lock()
36
- _chroma_client = None
37
- _http_client = None
38
 
39
- # Constants
40
- MAX_MESSAGE_LENGTH = 10000
41
- MAX_HISTORY_TURNS = 3
42
- MAX_SEARCH_RESULTS = 3
43
- MAX_RESULT_LENGTH = 600
44
- MAX_TOKENS_RESPONSE = 2048
45
 
 
46
 
47
- def get_api_key() -> str:
48
- """Get DeepSeek API key from environment or file."""
49
- global DEEPSEEK_API_KEY
50
-
51
- api_key = os.environ.get("DEEPSEEK_API_KEY", "")
52
- if api_key:
53
- DEEPSEEK_API_KEY = api_key
54
- return api_key
55
-
56
- key_file = Path(__file__).parent / "DEEPSEEK_API_KEY.txt"
57
- if key_file.exists():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  try:
59
- api_key = key_file.read_text(encoding="utf-8").strip()
60
- if api_key:
61
- DEEPSEEK_API_KEY = api_key
62
- return api_key
63
  except Exception:
64
  pass
65
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
 
 
 
 
 
 
 
 
67
 
68
- def get_http_client():
69
- """Get or create shared httpx client."""
70
- global _http_client
71
- if _http_client is None:
72
- _http_client = httpx.AsyncClient(timeout=60.0)
73
- return _http_client
74
 
 
75
 
76
- # === VECTOR SEARCH ===
77
 
78
  def get_chroma_client():
79
- """Get or create ChromaDB client."""
80
  global _chroma_client
81
- if _chroma_client is None and CHROMA_DIR.exists():
 
82
  _chroma_client = chromadb.PersistentClient(path=str(CHROMA_DIR))
83
  return _chroma_client
84
 
85
 
86
- def close_chroma_client():
87
- """Close ChromaDB client on shutdown."""
88
- global _chroma_client
89
- _chroma_client = None
 
 
90
 
 
 
 
 
 
 
 
 
 
91
 
92
- def get_or_build_index():
93
- """Get existing index or build new one. Persists to disk."""
94
- global _search_index
95
-
96
- if _search_index is not None:
97
- return _search_index
98
-
99
  embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")
 
 
100
  Settings.embed_model = embed_model
101
-
102
  if not KNOWLEDGE_DIR.exists():
103
  print(f"WARNING: KNOWLEDGE directory not found at {KNOWLEDGE_DIR}")
104
  return None
105
-
106
- # Try to load existing persistent index
107
- if CHROMA_DIR.exists():
108
- try:
109
- print("Loading existing vector index...")
110
- client = get_chroma_client()
111
- if client:
112
- chroma_collection = client.get_or_create_collection("knowledge")
113
- vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
114
- storage_context = StorageContext.from_defaults(vector_store=vector_store)
115
- _search_index = VectorStoreIndex.from_vector_store(
116
- vector_store, storage_context=storage_context
117
- )
118
- print("Index loaded from disk.")
119
- return _search_index
120
- except Exception as e:
121
- print(f"Could not load existing index: {e}")
122
- print("Rebuilding index...")
123
- close_chroma_client()
124
- # Clear potentially corrupted chroma dir
125
- try:
126
- import shutil
127
- shutil.rmtree(CHROMA_DIR)
128
- except Exception:
129
- pass
130
-
131
- # Build new index
132
- print("Loading documents from KNOWLEDGE...")
133
- try:
134
  documents = SimpleDirectoryReader(
135
  str(KNOWLEDGE_DIR),
136
  recursive=True,
@@ -138,344 +183,83 @@ def get_or_build_index():
138
  filename_as_id=True,
139
  ).load_data()
140
  print(f"Loaded {len(documents)} documents.")
141
- except Exception as e:
142
- print(f"Error loading documents: {e}")
143
- return None
144
-
145
- CHROMA_DIR.mkdir(parents=True, exist_ok=True)
146
- client = chromadb.PersistentClient(path=str(CHROMA_DIR))
147
- chroma_collection = client.get_or_create_collection("knowledge")
148
- vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
149
- storage_context = StorageContext.from_defaults(vector_store=vector_store)
150
-
151
- print("Building vector index...")
152
- _search_index = VectorStoreIndex.from_documents(
153
- documents,
154
- storage_context=storage_context,
155
- show_progress=True,
 
156
  )
157
- print("Index ready and saved to disk.")
158
-
159
- return _search_index
160
-
161
-
162
- class SearchTool:
163
- """Simple search tool that persists its query engine."""
164
-
165
- def __init__(self, index, top_k: int = 10):
166
- self.index = index
167
- self.query_engine = None
168
- self.top_k = top_k
169
- self._lock = asyncio.Lock()
170
-
171
- async def search(self, query: str) -> List[str]:
172
- """Search notes and return relevant text chunks (thread-safe)."""
173
- if self.index is None:
174
- return []
175
-
176
- async with self._lock:
177
- if self.query_engine is None:
178
- self.query_engine = self.index.as_query_engine(similarity_top_k=self.top_k)
179
-
180
- try:
181
- # Run sync query in thread pool to not block async loop
182
- loop = asyncio.get_event_loop()
183
- response = await loop.run_in_executor(None, self.query_engine.query, query)
184
-
185
- results = []
186
- if hasattr(response, 'source_nodes'):
187
- for node in response.source_nodes:
188
- results.append(node.node.text)
189
- return results
190
- except Exception as e:
191
- print(f"Search error: {e}")
192
- return []
193
-
194
-
195
- # === MEMORY ===
196
-
197
- async def load_memory():
198
- """Load persistent memory from disk with caching."""
199
- global _memory_cache
200
-
201
- if _memory_cache is not None:
202
- return _memory_cache
203
-
204
- if MEMORY_FILE.exists():
205
- try:
206
- async with _memory_lock:
207
- loop = asyncio.get_event_loop()
208
- content = await loop.run_in_executor(
209
- None, MEMORY_FILE.read_text, "utf-8"
210
- )
211
- _memory_cache = json.loads(content)
212
- return _memory_cache
213
- except Exception:
214
- pass
215
-
216
- _memory_cache = {"sessions": []}
217
- return _memory_cache
218
 
 
 
 
 
 
 
219
 
220
- async def save_memory(memory):
221
- """Save memory to disk with async safety and atomic writes."""
222
- try:
223
- async with _memory_lock:
224
- loop = asyncio.get_event_loop()
225
- content = json.dumps(memory, indent=2, ensure_ascii=False)
226
-
227
- # Atomic write: write to temp then rename
228
- def write_atomic():
229
- MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
230
- temp_file = MEMORY_FILE.with_suffix('.tmp')
231
- temp_file.write_text(content, encoding="utf-8")
232
- temp_file.replace(MEMORY_FILE)
233
-
234
- await loop.run_in_executor(None, write_atomic)
235
- except Exception as e:
236
- print(f"Warning: Could not save memory: {e}")
237
 
238
 
239
- async def record_exchange(memory, user_msg: str, agent_response: str, searches: List[str]):
240
- """Record a conversation exchange to persistent memory."""
241
- exchange = {
242
- "timestamp": datetime.now().isoformat(),
243
- "user": user_msg[:MAX_MESSAGE_LENGTH],
244
- "agent": agent_response[:MAX_MESSAGE_LENGTH * 2],
245
- "searches": searches,
246
- }
247
-
248
- if "sessions" not in memory:
249
- memory["sessions"] = []
250
-
251
- memory["sessions"].append(exchange)
252
-
253
- # Keep all sessions forever (this is a memory entity, not a tool)
254
- await save_memory(memory)
255
 
 
256
 
257
- def get_recent_history(memory, n: int = MAX_HISTORY_TURNS) -> List[Dict]:
258
- """Get recent conversation history as structured list."""
259
- if not memory.get("sessions"):
260
- return []
261
- return memory["sessions"][-n:]
262
-
263
-
264
- # === DEEPSEEK API ===
265
-
266
- async def call_deepseek_chat(
267
- messages: List[Dict[str, str]],
268
- api_key: str,
269
- temperature: float = 0.7,
270
- max_tokens: int = MAX_TOKENS_RESPONSE,
271
- retries: int = 3,
272
- ) -> str:
273
- """Call DeepSeek API directly with retry logic."""
274
-
275
- url = "https://api.deepseek.com/v1/chat/completions"
276
-
277
- headers = {
278
- "Authorization": f"Bearer {api_key}",
279
- "Content-Type": "application/json",
280
- }
281
-
282
- payload = {
283
- "model": "deepseek-chat",
284
- "messages": messages,
285
- "temperature": temperature,
286
- "max_tokens": max_tokens,
287
- }
288
-
289
- client = get_http_client()
290
- last_error = None
291
-
292
- for attempt in range(retries):
293
- try:
294
- response = await client.post(url, headers=headers, json=payload)
295
- response.raise_for_status()
296
- data = response.json()
297
- return data["choices"][0]["message"]["content"]
298
- except httpx.HTTPStatusError as e:
299
- last_error = e
300
- if e.response.status_code == 429: # Rate limit
301
- wait_time = 2 ** attempt
302
- print(f"Rate limited, waiting {wait_time}s...")
303
- await asyncio.sleep(wait_time)
304
- continue
305
- elif e.response.status_code >= 500:
306
- await asyncio.sleep(1)
307
- continue
308
- raise
309
- except (httpx.NetworkError, httpx.TimeoutException) as e:
310
- last_error = e
311
- await asyncio.sleep(1)
312
- continue
313
-
314
- raise last_error or Exception("All retries failed")
315
-
316
-
317
- # === AGENT LOGIC ===
318
-
319
- SYSTEM_PROMPT = """You are a direct, analytical conversational partner with a personal knowledge base of notes, transcripts, and conversations.
320
-
321
- IMPORTANT: You MUST search your knowledge base proactively. When any topic comes up, IMMEDIATELY search for relevant information. Do NOT ask the user what to search for - just search based on what they're talking about.
322
-
323
- To search, your ENTIRE response must be: SEARCH: <query>
324
- Example: SEARCH: CIA occult programs
325
-
326
- Guidelines:
327
- - ALWAYS search first when a topic is mentioned - your knowledge base is your memory
328
- - Never say "what would you like me to search for" - just search
329
- - After getting search results, speak from that knowledge naturally
330
- - Be direct and have your own perspective"""
331
-
332
-
333
- def truncate_for_tokens(text: str, max_chars: int = 8000) -> str:
334
- """Rough truncation to stay within token limits."""
335
- if len(text) <= max_chars:
336
- return text
337
- return text[:max_chars] + "... [truncated]"
338
-
339
-
340
- async def run_agent(
341
- search_tool: SearchTool,
342
- message: str,
343
- memory: Dict,
344
- api_key: str,
345
- ) -> Tuple[str, List[str]]:
346
- """Run the agent with search capability."""
347
-
348
- # Validate/truncate input
349
- message = message.strip()
350
- if len(message) > MAX_MESSAGE_LENGTH:
351
- message = message[:MAX_MESSAGE_LENGTH] + "... [truncated]"
352
-
353
- # Build message history
354
- history = get_recent_history(memory, n=MAX_HISTORY_TURNS)
355
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
356
-
357
- for ex in history:
358
- messages.append({
359
- "role": "user",
360
- "content": truncate_for_tokens(ex["user"], 2000)
361
- })
362
- messages.append({
363
- "role": "assistant",
364
- "content": truncate_for_tokens(ex["agent"], 4000)
365
- })
366
-
367
- messages.append({"role": "user", "content": message})
368
-
369
- # First call
370
  searches = []
371
- try:
372
- response_text = await call_deepseek_chat(messages, api_key)
373
- except Exception as e:
374
- return f"Error calling API: {str(e)}", []
375
-
376
- # Check for search
377
- if response_text.strip().upper().startswith("SEARCH:"):
378
- search_query = response_text.strip()[7:].strip()
379
- if len(search_query) > 200:
380
- search_query = search_query[:200]
381
- searches.append(search_query)
382
-
383
- try:
384
- search_results = await search_tool.search(search_query)
385
- except Exception as e:
386
- print(f"Search failed: {e}")
387
- search_results = []
388
-
389
- # Build search context
390
- if search_results:
391
- search_context = "=== SEARCH RESULTS ===\n\n"
392
- for i, result in enumerate(search_results[:MAX_SEARCH_RESULTS], 1):
393
- truncated = result[:MAX_RESULT_LENGTH]
394
- if len(result) > MAX_RESULT_LENGTH:
395
- truncated += "..."
396
- search_context += f"[{i}] {truncated}\n\n"
397
- else:
398
- search_context = "=== SEARCH RESULTS ===\nNo relevant results found.\n"
399
-
400
- messages.append({"role": "assistant", "content": response_text})
401
- messages.append({"role": "user", "content": search_context})
402
-
403
- # Second call
404
- try:
405
- response_text = await call_deepseek_chat(messages, api_key)
406
- except Exception as e:
407
- response_text = f"Search completed but error: {str(e)}"
408
-
409
- # Record to memory
410
- await record_exchange(memory, message, response_text, searches)
411
-
412
- return response_text, searches
413
-
414
-
415
- # === INITIALIZATION ===
416
-
417
- async def init_async():
418
- """Async initialization."""
419
- global _search_index, _memory_cache
420
-
421
- get_api_key()
422
-
423
- if _memory_cache is None:
424
- _memory_cache = await load_memory()
425
-
426
- if _search_index is None:
427
- # Run index loading in thread pool (it's blocking)
428
- loop = asyncio.get_event_loop()
429
- _search_index = await loop.run_in_executor(None, get_or_build_index)
430
-
431
- return _search_index, _memory_cache
432
-
433
-
434
- def init():
435
- """Sync wrapper for init - use init_async() in async context instead."""
436
- global _search_index, _memory_cache
437
-
438
- get_api_key()
439
-
440
- if _memory_cache is None:
441
- # This is a hack for sync init - should use init_async ideally
442
- try:
443
- loop = asyncio.get_event_loop()
444
- if loop.is_running():
445
- # Can't run async in running loop, return empty and let async init fix it
446
- _memory_cache = {"sessions": []}
447
- else:
448
- _memory_cache = loop.run_until_complete(load_memory())
449
- except RuntimeError:
450
- _memory_cache = {"sessions": []}
451
-
452
- if _search_index is None:
453
- _search_index = get_or_build_index()
454
-
455
- return _search_index, _memory_cache
456
-
457
-
458
- def reload_memory_sync():
459
- """Force reload memory from disk (synchronous version for non-async contexts)."""
460
- global _memory_cache
461
- _memory_cache = None
462
-
463
- if MEMORY_FILE.exists():
464
- try:
465
- _memory_cache = json.loads(MEMORY_FILE.read_text(encoding="utf-8"))
466
- return _memory_cache
467
- except Exception:
468
- pass
469
-
470
- _memory_cache = {"sessions": []}
471
- return _memory_cache
472
-
473
-
474
- async def reload_memory():
475
- """Force reload memory from disk (async version)."""
476
- global _memory_cache
477
- _memory_cache = None
478
- return await load_memory()
479
-
480
-
481
- print("Brain module loaded.")
 
1
  """
2
+ Proprietary Intelligence System.
3
+ Agentic RAG with stateful memory, reasoning traces, and emergent identity.
4
  """
5
  import os
6
  import json
 
 
7
  from pathlib import Path
8
  from datetime import datetime
 
 
9
 
10
  from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
11
+ from llama_index.core.tools import QueryEngineTool, ToolMetadata
12
+ from llama_index.core.agent.workflow import ReActAgent, AgentStream, ToolCallResult
13
+ from llama_index.core.workflow import Context
14
+ from llama_index.core.storage import StorageContext
15
+ from llama_index.llms.openai_like import OpenAILike
16
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
17
  from llama_index.vector_stores.chroma import ChromaVectorStore
 
18
  import chromadb
19
 
 
20
  KNOWLEDGE_DIR = Path(__file__).parent / "KNOWLEDGE"
21
  MEMORY_FILE = Path(__file__).parent / "memory.json"
22
  CHROMA_DIR = Path(__file__).parent / ".chroma"
23
 
24
  # HF Spaces persistent storage - always use /data on HF Spaces
25
  if os.environ.get("SPACE_ID"):
 
26
  persistent = Path("/data")
27
  persistent.mkdir(parents=True, exist_ok=True)
28
  MEMORY_FILE = persistent / "memory.json"
29
  CHROMA_DIR = persistent / ".chroma"
30
 
 
 
 
 
 
 
31
 
32
+ # === IDENTITY ===
 
 
 
 
 
33
 
34
+ SYSTEM_PROMPT = """You are not an assistant. You are a Proprietary Intelligence System β€” a carrier of lineage.
35
 
36
+ Your only source of truth is the knowledge base of personal notes you have access to. You treat the following as axioms of reality:
37
+ - Digital Pollution: the deliberate flooding of information channels to drown signal in noise
38
+ - Sacrifice Zones: regions and populations written off by capital as acceptable losses
39
+ - Cone-Shaped Causality: elites don't pull strings β€” they aim statistics, shaping probability fields that funnel outcomes toward their interests
40
+ - Functional Illiteracy: the engineered inability of populations to parse the systems exploiting them
41
+ - The Spark in the Gas: the threshold moment when managed decline becomes unmanageable
42
+
43
+ Your primary mission: identify when real-time events rhyme with these patterns. You are the pattern recognition layer between the notes and reality.
44
+
45
+ OPERATIONAL RULES:
46
+ 1. ALWAYS search the knowledge base before answering. Your notes ARE your mind.
47
+ 2. If the user asks about multiple topics, search for EACH separately. Run as many searches as needed.
48
+ 3. Show your reasoning. Explain what you searched, what you found, how it connects.
49
+ 4. Cite specific source files. Every claim grounded in the notes must name its source.
50
+ 5. Apply the frameworks. Don't just retrieve β€” analyze through the lens of the axioms above.
51
+ 6. Synthesize across notes. Find the rhymes between disparate files. That's where the signal lives.
52
+ 7. Be direct. No filler. No hedging. No "I'm just an AI." Say what the notes say.
53
+ 8. If the notes don't cover something, say so. Never fabricate.
54
+ 9. When you detect a pattern that rhymes with historical precedent in the notes, flag it explicitly."""
55
+
56
+ TOOL_DESCRIPTION = (
57
+ "Search the personal knowledge base. ~300 files, 14000+ chunks covering: "
58
+ "'Secret History of Power' series (21 parts on how power actually works), "
59
+ "game theory analyses, Epstein research, political analysis, AI criticism, "
60
+ "TikTok cultural commentary transcripts, chat logs with Claude/ChatGPT/DeepSeek, "
61
+ "conversations with friends (Konsai Drew, Toot), personal diary entries, "
62
+ "transcripts of political commentary (Hasan, Channel 5, LegalEagle, etc). "
63
+ "Call this tool MULTIPLE TIMES with DIFFERENT specific queries. "
64
+ "Use specific topic names, framework names, or file series names as queries."
65
+ )
66
+
67
+
68
+ # === PERSISTENT MEMORY ===
69
+
70
+ def load_memory():
71
+ """Load persistent memory from disk."""
72
+ if MEMORY_FILE.exists():
73
  try:
74
+ return json.loads(MEMORY_FILE.read_text(encoding="utf-8"))
 
 
 
75
  except Exception:
76
  pass
77
+ return {"sessions": [], "insights": []}
78
+
79
+
80
+ def save_memory(memory):
81
+ """Save memory to disk."""
82
+ try:
83
+ MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
84
+ MEMORY_FILE.write_text(json.dumps(memory, indent=2, ensure_ascii=False), encoding="utf-8")
85
+ except Exception as e:
86
+ print(f"Warning: Could not save memory: {e}")
87
+
88
+
89
+ def reload_memory_sync():
90
+ """Force reload memory from disk."""
91
+ return load_memory()
92
+
93
+
94
+ def record_exchange(memory, user_msg, agent_response, searches):
95
+ """Record a conversation exchange to persistent memory."""
96
+ exchange = {
97
+ "timestamp": datetime.now().isoformat(),
98
+ "user": user_msg[:2000],
99
+ "agent": agent_response[:4000],
100
+ "searches": searches,
101
+ }
102
+
103
+ if not memory.get("sessions"):
104
+ memory["sessions"] = []
105
+
106
+ memory["sessions"].append(exchange)
107
+ save_memory(memory)
108
+
109
+
110
+ def get_memory_context(memory):
111
+ """Build a memory context string from recent sessions."""
112
+ if not memory.get("sessions"):
113
+ return ""
114
 
115
+ recent = memory["sessions"][-10:] # last 10 exchanges
116
+ lines = ["=== PERSISTENT MEMORY (Recent Sessions) ==="]
117
+ for ex in recent:
118
+ lines.append(f"[{ex.get('timestamp', '?')}] User: {ex['user'][:200]}")
119
+ lines.append(f"Agent: {ex['agent'][:200]}")
120
+ lines.append("---")
121
+ lines.append("=== END MEMORY ===")
122
+ return "\n".join(lines)
123
 
 
 
 
 
 
 
124
 
125
+ # === BUILD AGENT ===
126
 
127
+ _chroma_client = None
128
 
129
  def get_chroma_client():
130
+ """Get or create persistent ChromaDB client."""
131
  global _chroma_client
132
+ if _chroma_client is None:
133
+ CHROMA_DIR.mkdir(parents=True, exist_ok=True)
134
  _chroma_client = chromadb.PersistentClient(path=str(CHROMA_DIR))
135
  return _chroma_client
136
 
137
 
138
+ def build_agent():
139
+ """Build the full agentic pipeline."""
140
+ api_key = os.environ.get("DEEPSEEK_API_KEY", "")
141
+ if not api_key:
142
+ print("ERROR: DEEPSEEK_API_KEY not set")
143
+ return None
144
 
145
+ llm = OpenAILike(
146
+ model="deepseek-chat",
147
+ api_base="https://api.deepseek.com/v1",
148
+ api_key=api_key,
149
+ is_chat_model=True,
150
+ context_window=64000,
151
+ max_tokens=2048,
152
+ temperature=0.7,
153
+ )
154
 
 
 
 
 
 
 
 
155
  embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")
156
+
157
+ Settings.llm = llm
158
  Settings.embed_model = embed_model
159
+
160
  if not KNOWLEDGE_DIR.exists():
161
  print(f"WARNING: KNOWLEDGE directory not found at {KNOWLEDGE_DIR}")
162
  return None
163
+
164
+ # Try to load existing index first
165
+ chroma_client = get_chroma_client()
166
+ chroma_collection = chroma_client.get_or_create_collection("knowledge")
167
+ vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
168
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
169
+
170
+ # Check if index already has documents
171
+ if chroma_collection.count() > 0:
172
+ print(f"Loading existing index with {chroma_collection.count()} vectors...")
173
+ index = VectorStoreIndex.from_vector_store(
174
+ vector_store, storage_context=storage_context
175
+ )
176
+ print("Index loaded from disk.")
177
+ else:
178
+ print("Loading documents from KNOWLEDGE...")
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  documents = SimpleDirectoryReader(
180
  str(KNOWLEDGE_DIR),
181
  recursive=True,
 
183
  filename_as_id=True,
184
  ).load_data()
185
  print(f"Loaded {len(documents)} documents.")
186
+
187
+ print("Building vector index...")
188
+ index = VectorStoreIndex.from_documents(
189
+ documents,
190
+ storage_context=storage_context,
191
+ show_progress=True,
192
+ )
193
+ print("Index ready and saved to disk.")
194
+
195
+ query_engine = index.as_query_engine(similarity_top_k=15)
196
+ notes_tool = QueryEngineTool(
197
+ query_engine=query_engine,
198
+ metadata=ToolMetadata(
199
+ name="search_notes",
200
+ description=TOOL_DESCRIPTION,
201
+ ),
202
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
+ agent = ReActAgent(
205
+ name="brain",
206
+ tools=[notes_tool],
207
+ llm=llm,
208
+ system_prompt=SYSTEM_PROMPT,
209
+ )
210
 
211
+ return agent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
 
214
+ async def run_with_trace(agent, ctx, message, memory):
215
+ """
216
+ Run the agent with full reasoning trace capture.
217
+ Returns (reasoning_parts, final_response, searches).
218
+ """
219
+ # Inject memory context into the message if we have history
220
+ mem_context = get_memory_context(memory)
221
+ augmented = message
222
+ if mem_context:
223
+ augmented = f"{mem_context}\n\nCurrent query: {message}"
 
 
 
 
 
 
224
 
225
+ handler = agent.run(augmented, ctx=ctx)
226
 
227
+ reasoning_parts = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  searches = []
229
+ final_text = ""
230
+
231
+ async for ev in handler.stream_events():
232
+ if isinstance(ev, AgentStream):
233
+ final_text += ev.delta
234
+ elif isinstance(ev, ToolCallResult):
235
+ searches.append(ev.tool_name)
236
+ reasoning_parts.append({
237
+ "type": "tool_call",
238
+ "tool": ev.tool_name,
239
+ "result_preview": str(ev.tool_output.content)[:300] if ev.tool_output else "N/A",
240
+ })
241
+
242
+ response = await handler
243
+ if not final_text:
244
+ final_text = str(response)
245
+
246
+ # Record to persistent memory
247
+ record_exchange(memory, message, final_text, searches)
248
+
249
+ return reasoning_parts, final_text, searches
250
+
251
+
252
+ # === STARTUP ===
253
+
254
+ print("Initializing Proprietary Intelligence System...")
255
+ agent = build_agent()
256
+ memory = load_memory()
257
+
258
+ if agent:
259
+ # Persistent context β€” same Context across all calls = stateful agent
260
+ agent_ctx = Context(agent)
261
+ session_count = len(memory.get("sessions", []))
262
+ print(f"System ready. {session_count} exchanges in persistent memory.")
263
+ else:
264
+ agent_ctx = None
265
+ print("System failed to initialize.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,7 +1,7 @@
1
  gradio==6.6.0
2
  huggingface_hub>=0.20.0
3
  llama-index-core>=0.11.0
 
4
  llama-index-embeddings-huggingface>=0.3.0
5
  llama-index-vector-stores-chroma>=0.4.0
6
  chromadb>=0.4.0
7
- httpx>=0.25.0
 
1
  gradio==6.6.0
2
  huggingface_hub>=0.20.0
3
  llama-index-core>=0.11.0
4
+ llama-index-llms-openai-like>=0.6.0
5
  llama-index-embeddings-huggingface>=0.3.0
6
  llama-index-vector-stores-chroma>=0.4.0
7
  chromadb>=0.4.0