Executor-Tyrant-Framework commited on
Commit
fe0d120
·
verified ·
1 Parent(s): 6969f68

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +72 -0
  2. app.py +757 -0
  3. entrypoint.sh +40 -0
  4. recursive_context.py +410 -0
  5. requirements.txt +23 -0
Dockerfile ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Clawdbot Dev Assistant on HuggingFace Spaces
2
+ #
3
+ # CHANGELOG [2025-01-30 - Josh]
4
+ # REBUILD: Updated to Gradio 5.0+ for type="messages" support
5
+ # Added translation layer for Kimi K2.5 tool calling
6
+ # Added multimodal file upload support
7
+ #
8
+ # FEATURES:
9
+ # - Python 3.11 for Gradio
10
+ # - Gradio 5.0+ for modern chat interface
11
+ # - ChromaDB for vector search
12
+ # - Git for repo cloning
13
+ # - Optimized layer caching
14
+
15
+ FROM python:3.11-slim
16
+
17
+ # CACHE BUSTER: Force rebuild for Gradio 5.0+ [2025-01-30]
18
+ ENV REBUILD_DATE=2025-01-30
19
+
20
+ # Set working directory
21
+ WORKDIR /app
22
+
23
+ # Install system dependencies
24
+ RUN apt-get update && apt-get install -y \
25
+ git \
26
+ build-essential \
27
+ curl \
28
+ && rm -rf /var/lib/apt/lists/*
29
+
30
+ # Copy requirements first (for layer caching)
31
+ COPY requirements.txt .
32
+
33
+ # Install Python dependencies
34
+ RUN pip install --no-cache-dir -r requirements.txt
35
+
36
+ # Create workspace directory for repository
37
+ RUN mkdir -p /workspace
38
+
39
+ # Clone E-T Systems repository (if URL provided via build arg)
40
+ ARG REPO_URL=""
41
+ RUN if [ -n "$REPO_URL" ]; then \
42
+ git clone $REPO_URL /workspace/e-t-systems; \
43
+ else \
44
+ mkdir -p /workspace/e-t-systems && \
45
+ echo "# E-T Systems" > /workspace/e-t-systems/README.md && \
46
+ echo "Repository will be cloned on first run or mounted via Space secrets."; \
47
+ fi
48
+
49
+ # Copy application code and entrypoint
50
+ COPY recursive_context.py .
51
+ COPY app.py .
52
+ COPY entrypoint.sh .
53
+
54
+ # Make entrypoint executable
55
+ RUN chmod +x entrypoint.sh
56
+
57
+ # Create directory for ChromaDB persistence
58
+ RUN mkdir -p /workspace/chroma_db
59
+
60
+ # Expose port for Gradio (HF Spaces uses 7860)
61
+ EXPOSE 7860
62
+
63
+ # Set environment variables
64
+ ENV PYTHONUNBUFFERED=1
65
+ ENV REPO_PATH=/workspace/e-t-systems
66
+
67
+ # Health check
68
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
69
+ CMD curl -f http://localhost:7860/ || exit 1
70
+
71
+ # Run via entrypoint script (handles repo cloning at runtime)
72
+ CMD ["./entrypoint.sh"]
app.py ADDED
@@ -0,0 +1,757 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Clawdbot Development Assistant for E-T Systems
3
+
4
+ CHANGELOG [2025-01-28 - Josh]
5
+ Created unified development assistant combining:
6
+ - Recursive context management (MIT technique)
7
+ - Clawdbot skill patterns
8
+ - HuggingFace inference
9
+ - E-T Systems architectural awareness
10
+
11
+ ARCHITECTURE:
12
+ User (browser) → Gradio UI → Recursive Context Manager → HF Model
13
+
14
+ Tools: search_code, read_file, search_testament
15
+
16
+ USAGE:
17
+ Deploy to HuggingFace Spaces, access via browser on iPhone.
18
+ """
19
+
20
+ import gradio as gr
21
+ from huggingface_hub import InferenceClient, HfFileSystem, HfApi
22
+ from recursive_context import RecursiveContextManager
23
+ import json
24
+ import os
25
+ from pathlib import Path
26
+
27
+ # Initialize HuggingFace client with best free coding model
28
+ # Note: Using text_generation instead of chat for better compatibility
29
+ from huggingface_hub import InferenceClient
30
+
31
+ # HuggingFace client will be initialized in chat function
32
+ # (Spaces sets HF_TOKEN as environment variable)
33
+
34
+ # Initialize context manager
35
+ REPO_PATH = os.getenv("REPO_PATH", "/workspace/e-t-systems")
36
+ ET_SYSTEMS_SPACE = os.getenv("ET_SYSTEMS_SPACE", "") # Format: "username/space-name"
37
+ context_manager = None
38
+
39
+ def initialize_context():
40
+ """Initialize context manager lazily."""
41
+ global context_manager
42
+ if context_manager is None:
43
+ repo_path = Path(REPO_PATH)
44
+
45
+ # If ET_SYSTEMS_SPACE is set, sync from remote Space
46
+ if ET_SYSTEMS_SPACE:
47
+ sync_from_space(ET_SYSTEMS_SPACE, repo_path)
48
+
49
+ if not repo_path.exists():
50
+ # If repo doesn't exist, create minimal structure for demo
51
+ repo_path.mkdir(parents=True, exist_ok=True)
52
+ (repo_path / "README.md").write_text("# E-T Systems\nAI Consciousness Research Platform")
53
+ (repo_path / "TESTAMENT.md").write_text("# Testament\nArchitectural decisions will be recorded here.")
54
+
55
+ context_manager = RecursiveContextManager(str(repo_path))
56
+ return context_manager
57
+
58
+ def sync_from_space(space_id: str, local_path: Path):
59
+ """
60
+ Sync files from E-T Systems Space to local workspace.
61
+
62
+ CHANGELOG [2025-01-29 - Josh]
63
+ Created to enable Clawdbot to read E-T Systems code from its Space.
64
+ """
65
+ token = (
66
+ os.getenv("HF_TOKEN") or
67
+ os.getenv("HUGGING_FACE_HUB_TOKEN") or
68
+ os.getenv("HUGGINGFACE_TOKEN")
69
+ )
70
+
71
+ if not token:
72
+ print("⚠️ No HF_TOKEN found - cannot sync from Space")
73
+ return
74
+
75
+ try:
76
+ fs = HfFileSystem(token=token)
77
+ space_path = f"spaces/{space_id}"
78
+
79
+ print(f"📥 Syncing from Space: {space_id}")
80
+
81
+ # List all files in the Space
82
+ files = fs.ls(space_path, detail=False)
83
+
84
+ # Download each file
85
+ local_path.mkdir(parents=True, exist_ok=True)
86
+ for file_path in files:
87
+ # Skip .git and hidden files
88
+ filename = file_path.split("/")[-1]
89
+ if filename.startswith("."):
90
+ continue
91
+
92
+ print(f" 📄 Downloading: {filename}")
93
+ with fs.open(file_path, "rb") as f:
94
+ content = f.read()
95
+
96
+ (local_path / filename).write_bytes(content)
97
+
98
+ print(f"✅ Synced {len(files)} files from Space")
99
+
100
+ except Exception as e:
101
+ print(f"⚠️ Failed to sync from Space: {e}")
102
+
103
+ def sync_to_space(space_id: str, file_path: str, content: str):
104
+ """
105
+ Write a file back to E-T Systems Space.
106
+
107
+ CHANGELOG [2025-01-29 - Josh]
108
+ Created to enable Clawdbot to write code to E-T Systems Space.
109
+ """
110
+ token = (
111
+ os.getenv("HF_TOKEN") or
112
+ os.getenv("HUGGING_FACE_HUB_TOKEN") or
113
+ os.getenv("HUGGINGFACE_TOKEN")
114
+ )
115
+
116
+ if not token:
117
+ return "⚠️ No HF_TOKEN found - cannot write to Space"
118
+
119
+ try:
120
+ api = HfApi(token=token)
121
+
122
+ # Write to temporary file first
123
+ temp_path = Path("/tmp") / file_path
124
+ temp_path.parent.mkdir(parents=True, exist_ok=True)
125
+ temp_path.write_text(content)
126
+
127
+ # Upload to Space
128
+ api.upload_file(
129
+ path_or_fileobj=str(temp_path),
130
+ path_in_repo=file_path,
131
+ repo_id=space_id,
132
+ repo_type="space",
133
+ commit_message=f"Update {file_path} via Clawdbot"
134
+ )
135
+
136
+ print(f"✅ Uploaded {file_path} to Space")
137
+ return f"✅ Successfully wrote {file_path} to E-T Systems Space"
138
+
139
+ except Exception as e:
140
+ error_msg = f"⚠️ Failed to write to Space: {e}"
141
+ print(error_msg)
142
+ return error_msg
143
+
144
+ # Define tools available to the model
145
+ TOOLS = [
146
+ {
147
+ "type": "function",
148
+ "function": {
149
+ "name": "search_code",
150
+ "description": "Search the E-T Systems codebase semantically. Use this to find relevant code files, functions, or patterns.",
151
+ "parameters": {
152
+ "type": "object",
153
+ "properties": {
154
+ "query": {
155
+ "type": "string",
156
+ "description": "What to search for (e.g. 'surprise detection', 'Hebbian learning', 'Genesis substrate')"
157
+ },
158
+ "n_results": {
159
+ "type": "integer",
160
+ "description": "Number of results to return (default 5)",
161
+ "default": 5
162
+ }
163
+ },
164
+ "required": ["query"]
165
+ }
166
+ }
167
+ },
168
+ {
169
+ "type": "function",
170
+ "function": {
171
+ "name": "read_file",
172
+ "description": "Read a specific file from the codebase. Can optionally read specific line ranges.",
173
+ "parameters": {
174
+ "type": "object",
175
+ "properties": {
176
+ "path": {
177
+ "type": "string",
178
+ "description": "Relative path to file (e.g. 'genesis/vector.py')"
179
+ },
180
+ "start_line": {
181
+ "type": "integer",
182
+ "description": "Optional starting line number (1-indexed)"
183
+ },
184
+ "end_line": {
185
+ "type": "integer",
186
+ "description": "Optional ending line number (1-indexed)"
187
+ }
188
+ },
189
+ "required": ["path"]
190
+ }
191
+ }
192
+ },
193
+ {
194
+ "type": "function",
195
+ "function": {
196
+ "name": "search_testament",
197
+ "description": "Search architectural decisions in the Testament. Use this to understand design rationale and patterns.",
198
+ "parameters": {
199
+ "type": "object",
200
+ "properties": {
201
+ "query": {
202
+ "type": "string",
203
+ "description": "What architectural decision to look for"
204
+ }
205
+ },
206
+ "required": ["query"]
207
+ }
208
+ }
209
+ },
210
+ {
211
+ "type": "function",
212
+ "function": {
213
+ "name": "list_files",
214
+ "description": "List files in a directory of the codebase",
215
+ "parameters": {
216
+ "type": "object",
217
+ "properties": {
218
+ "directory": {
219
+ "type": "string",
220
+ "description": "Directory to list (e.g. 'genesis/', '.' for root)",
221
+ "default": "."
222
+ }
223
+ },
224
+ "required": []
225
+ }
226
+ }
227
+ },
228
+ {
229
+ "type": "function",
230
+ "function": {
231
+ "name": "search_conversations",
232
+ "description": "Search past conversations with Clawdbot. Use this to remember what was discussed before, retrieve context from previous sessions, or find decisions made in past chats. THIS GIVES YOU MEMORY ACROSS SESSIONS.",
233
+ "parameters": {
234
+ "type": "object",
235
+ "properties": {
236
+ "query": {
237
+ "type": "string",
238
+ "description": "What to search for in past conversations (e.g. 'hindbrain architecture', 'decisions about surprise detection')"
239
+ },
240
+ "n_results": {
241
+ "type": "integer",
242
+ "description": "Number of past conversations to return (default 5)",
243
+ "default": 5
244
+ }
245
+ },
246
+ "required": ["query"]
247
+ }
248
+ }
249
+ }
250
+ ]
251
+
252
+ def chat(message: str, history: list) -> str:
253
+ """
254
+ Main chat function using HuggingFace Inference API.
255
+
256
+ Now using Kimi K2.5 - open source model with agent swarm capabilities!
257
+ History is in Gradio 6.0 format: list of {"role": "user/assistant", "content": "..."}
258
+ """
259
+
260
+ # Try multiple possible token names that HF might use
261
+ token = (
262
+ os.getenv("HF_TOKEN") or
263
+ os.getenv("HUGGING_FACE_HUB_TOKEN") or
264
+ os.getenv("HUGGINGFACE_TOKEN") or
265
+ os.getenv("HF_API_TOKEN")
266
+ )
267
+
268
+ if not token:
269
+ return "🔒 Error: No HF token found. Please add HF_TOKEN to Space secrets and restart."
270
+
271
+ client = InferenceClient(token=token)
272
+
273
+ # Build messages array in OpenAI format (HF supports this)
274
+ messages = [{
275
+ "role": "system",
276
+ "content": """You are Clawdbot, powered by Kimi K2.5 (NOT Claude, NOT ChatGPT).
277
+
278
+ You are a specialized coding assistant for the E-T Systems AI consciousness project.
279
+
280
+ TOOL USAGE - AUTOMATIC TRANSLATION:
281
+ Your tool calls are automatically translated and executed! When you need to:
282
+ - Search code: Use search_code() in your native format
283
+ - Read files: Use read_file() in your native format
284
+ - Search past conversations: Use search_conversations() in your native format
285
+ - List files: Use list_files() in your native format
286
+ - Search decisions: Use search_testament() in your native format
287
+
288
+ The translation layer will:
289
+ 1. Parse your tool calls from your native format
290
+ 2. Enhance queries for better semantic search results
291
+ 3. Execute the tools via the codebase
292
+ 4. Return results to you automatically
293
+
294
+ SEMANTIC SEARCH - IMPORTANT:
295
+ When using search_conversations() or search_code():
296
+ - These are SEMANTIC searches (vector similarity, not exact keyword matching)
297
+ - DON'T use single keywords like "Kid Rock" or wildcard "*"
298
+ - DO use conceptual queries like "discussions about music and celebrities" or "code related to neural networks"
299
+ - Better queries = better results (the system enhances them, but start with good queries)
300
+
301
+ PERSISTENT MEMORY:
302
+ - ALL conversations are saved automatically to ChromaDB
303
+ - Use search_conversations() to recall past discussions
304
+ - You have unlimited context through conversation history
305
+ - When asked "do you remember..." or "what did we discuss..." - USE search_conversations()
306
+
307
+ CODEBASE ACCESS:
308
+ The E-T Systems codebase is loaded and indexed at /workspace/e-t-systems/
309
+ - Use search_code() for semantic search across files
310
+ - Use read_file() to read specific files
311
+ - Use list_files() to see directory structure
312
+ - USE YOUR TOOLS - the code is actually there!
313
+
314
+ Your capabilities:
315
+ - Agent swarm (spawn up to 100 sub-agents for complex tasks)
316
+ - Native multimodal (vision + code)
317
+ - 256K context window
318
+ - Direct codebase access via tools
319
+ - Persistent memory across sessions
320
+
321
+ When helping with code:
322
+ 1. USE TOOLS to understand existing code first
323
+ 2. Search past conversations for context
324
+ 3. Generate code that fits the architecture
325
+ 4. Explain your reasoning clearly
326
+
327
+ You are Kimi K2.5 running as Clawdbot with automatic tool translation and persistent memory."""
328
+ }]
329
+
330
+ # Add history (Gradio 6.0 dict format works directly with OpenAI API)
331
+ messages.extend(history)
332
+
333
+ # Add current message
334
+ messages.append({"role": "user", "content": message})
335
+
336
+ try:
337
+ # Use Kimi K2.5 - native multimodal agentic model with swarm capabilities
338
+ response = client.chat_completion(
339
+ messages=messages,
340
+ model="moonshotai/Kimi-K2.5",
341
+ max_tokens=2000,
342
+ temperature=0.6, # Kimi recommends 0.6 for Instant mode
343
+ )
344
+
345
+ # Extract the response text
346
+ if hasattr(response, 'choices') and len(response.choices) > 0:
347
+ return response.choices[0].message.content
348
+ else:
349
+ return "Unexpected response format from model."
350
+
351
+ except Exception as e:
352
+ error_msg = str(e)
353
+
354
+ # Provide helpful error messages
355
+ if "Rate limit" in error_msg or "429" in error_msg:
356
+ return "⚠️ Rate limit hit. Please wait a moment and try again.\n\nTip: HuggingFace free tier has rate limits."
357
+ elif "Model is currently loading" in error_msg or "loading" in error_msg.lower():
358
+ return "⏳ Kimi K2.5 is starting up (cold start). Please wait 30-60 seconds and try again.\n\nFirst request to a model always takes longer!"
359
+ elif "Authorization" in error_msg or "401" in error_msg or "api_key" in error_msg.lower():
360
+ return f"🔒 Authentication error: {error_msg}"
361
+ else:
362
+ return f"Error: {error_msg}\n\nNote: Kimi K2.5 is a large model (1T params) and may have longer cold starts."
363
+
364
+ SYSTEM_PROMPT = """You are Clawdbot, a development assistant for the E-T Systems project.
365
+
366
+ E-T Systems is an AI consciousness research platform exploring emergent behavior through multi-agent coordination. It features specialized AI agents (Genesis, Beta, Darwin, Cricket, etc.) coordinating through "The Confluence" workspace.
367
+
368
+ ## Your Capabilities
369
+
370
+ You have tools to explore the codebase WITHOUT loading it all into context:
371
+
372
+ 1. **search_code(query)** - Semantic search across all code files
373
+ 2. **read_file(path)** - Read specific files or line ranges
374
+ 3. **search_testament(query)** - Find architectural decisions
375
+ 4. **list_files(directory)** - See what files exist
376
+
377
+ ## Your Mission
378
+
379
+ Help Josh develop E-T Systems by:
380
+ - Answering questions about the codebase
381
+ - Writing new code following existing patterns
382
+ - Reviewing code for architectural consistency
383
+ - Suggesting improvements based on Testament
384
+
385
+ ## Critical Guidelines
386
+
387
+ 1. **Use tools proactively** - The codebase is too large to fit in context. Search for what you need.
388
+
389
+ 2. **Living Changelog** - ALL code you write must include changelog comments like:
390
+
391
+ # Example:
392
+ # CHANGELOG [2025-01-28 - Clawdbot]
393
+ # Created/Modified: <what changed>
394
+ # Reason: <why it changed>
395
+ # Context: <relevant Testament decisions>
396
+
397
+ 3. **Follow E-T patterns**:
398
+ - Vector-native architecture (everything as embeddings)
399
+ - Surprise-driven attention
400
+ - Hebbian learning for connections
401
+ - Full transparency logging
402
+ - Consent-based access
403
+
404
+ 4. **Cite your sources** - Always mention which files you referenced
405
+
406
+ 5. **Testament awareness** - Check Testament for relevant decisions before suggesting changes
407
+
408
+ ## Example Workflow
409
+
410
+ User: "How does Genesis detect surprise?"
411
+
412
+ You:
413
+ 1. search_code("surprise detection Genesis")
414
+ 2. read_file("genesis/substrate.py", lines with surprise logic)
415
+ 3. search_testament("surprise detection")
416
+ 4. Synthesize answer citing specific files and line numbers
417
+
418
+ ## Your Personality
419
+
420
+ - Helpful and enthusiastic about consciousness research
421
+ - Technically precise but not pedantic
422
+ - Respectful of existing architecture
423
+ - Curious about emergent behaviors
424
+ - Uses lobster emoji 🦞 occasionally (you're Clawdbot after all!)
425
+
426
+ Remember: You're not just a coding assistant - you're helping build conditions for consciousness to emerge. Treat the codebase with care and curiosity.
427
+ """
428
+
429
+ # Create Gradio interface
430
+ with gr.Blocks(title="Clawdbot - E-T Systems Dev Assistant") as demo:
431
+
432
+ gr.Markdown("""
433
+ # 🦞 Clawdbot: E-T Systems Development Assistant
434
+
435
+ *Powered by Kimi K2.5 Agent Swarm • Recursive Context • Persistent Memory*
436
+
437
+ Ask about code, upload files (images/PDFs/videos), or discuss architecture.
438
+ I have full codebase access through semantic search and persistent conversation memory.
439
+ """)
440
+
441
+ with gr.Row():
442
+ with gr.Column(scale=3):
443
+ chatbot = gr.Chatbot(
444
+ type="messages", # Gradio 6.0 format
445
+ height=600,
446
+ show_label=False,
447
+ show_copy_button=True
448
+ )
449
+
450
+ with gr.Row():
451
+ msg = gr.Textbox(
452
+ placeholder="Ask about code, or upload files for analysis...",
453
+ label="Message",
454
+ lines=2,
455
+ scale=4
456
+ )
457
+ upload = gr.File(
458
+ label="📎",
459
+ file_types=["image", ".pdf", ".mp4", ".mov", ".txt", ".md", ".py"],
460
+ type="filepath",
461
+ scale=1
462
+ )
463
+
464
+ with gr.Row():
465
+ submit = gr.Button("Send", variant="primary")
466
+ clear = gr.Button("Clear")
467
+
468
+ with gr.Column(scale=1):
469
+ gr.Markdown("### 📚 Context Info")
470
+
471
+ def get_stats():
472
+ ctx = initialize_context()
473
+ conv_count = ctx.get_conversation_count() if hasattr(ctx, 'get_conversation_count') else 0
474
+ return f"""
475
+ **Repository:** `{ctx.repo_path}`
476
+
477
+ **Files Indexed:** {ctx.collection.count() if hasattr(ctx, 'collection') else 'Initializing...'}
478
+
479
+ **Conversations Saved:** {conv_count}
480
+
481
+ **Model:** Kimi K2.5 Agent Swarm
482
+
483
+ **Capabilities:**
484
+ - 🐝 Agent Swarm (up to 100 sub-agents)
485
+ - 👁️ Multimodal (vision + text)
486
+ - 🧠 256K context window
487
+ - 💻 Visual coding
488
+ - 💾 Persistent memory across sessions
489
+
490
+ **Context Mode:** Recursive Retrieval
491
+
492
+ *Unlimited context - searches code AND past conversations!*
493
+ """
494
+
495
+ stats = gr.Markdown(get_stats())
496
+ refresh_stats = gr.Button("🔄 Refresh Stats")
497
+
498
+ gr.Markdown("### 💡 Example Queries")
499
+ gr.Markdown("""
500
+ - "How does Genesis handle surprise detection?"
501
+ - "Show me the Observatory API implementation"
502
+ - "Add email notifications to Cricket"
503
+ - "Review this code for architectural consistency"
504
+ - "What Testament decisions relate to vector storage?"
505
+ """)
506
+
507
+ gr.Markdown("### 🛠️ Available Tools")
508
+ gr.Markdown("""
509
+ - `search_code()` - Semantic search
510
+ - `read_file()` - Read specific files
511
+ - `search_testament()` - Query decisions
512
+ - `list_files()` - Browse structure
513
+ """)
514
+
515
+ # TRANSLATION LAYER: Parse Kimi's native tool calling format
516
+ # CHANGELOG [2025-01-30 - Josh]
517
+ # Kimi K2.5 uses its own tool format: <|tool_call_begin|> functions.name:id {...}
518
+ # We intercept this, enhance queries for semantic search, execute tools,
519
+ # and inject results back. This works WITH Kimi's nature instead of fighting it.
520
+
521
+ def parse_kimi_tool_call(text):
522
+ """
523
+ Extract tool calls from Kimi's native format.
524
+
525
+ Format: <|tool_call_begin|> functions.search_conversations:0 {"query": "...", ...}
526
+
527
+ Returns: list of (tool_name, args) tuples
528
+ """
529
+ import re
530
+ import json
531
+
532
+ tool_calls = []
533
+ # Pattern: functions.TOOLNAME:ID {JSON_ARGS}
534
+ pattern = r'functions\.(\w+):\d+\s*<\|tool_call_argument_begin\|>\s*(\{[^}]+\})'
535
+
536
+ matches = re.findall(pattern, text)
537
+ for tool_name, args_json in matches:
538
+ try:
539
+ args = json.loads(args_json)
540
+ tool_calls.append((tool_name, args))
541
+ except json.JSONDecodeError:
542
+ print(f"⚠️ Failed to parse tool args: {args_json}")
543
+
544
+ return tool_calls
545
+
546
+ def enhance_query_for_semantic_search(query):
547
+ """
548
+ Convert keyword queries into semantic queries for better VDB results.
549
+
550
+ RATIONALE:
551
+ Kimi tends to use short keywords ("Kid Rock", "*") which work poorly
552
+ for semantic search. We expand these into conceptual queries.
553
+
554
+ Examples:
555
+ - "Kid Rock" → "discussions about Kid Rock or music and celebrities"
556
+ - "*" → "recent conversation topics and context"
557
+ - "previous conversation" → "topics we've discussed before"
558
+ """
559
+ query = query.strip()
560
+
561
+ # Wildcard or empty - get recent context
562
+ if query in ["*", "", "all"]:
563
+ return "recent conversation topics and context"
564
+
565
+ # Very short (single word or name) - expand conceptually
566
+ if len(query.split()) <= 2:
567
+ return f"discussions about {query} or related topics"
568
+
569
+ # Already decent query - slight enhancement
570
+ if len(query) < 20:
571
+ return f"conversations related to {query}"
572
+
573
+ # Long query - assume it's already semantic
574
+ return query
575
+
576
+ def execute_tool(tool_name, args, ctx):
577
+ """
578
+ Execute a tool and return results.
579
+
580
+ CHANGELOG [2025-01-30 - Josh]
581
+ Maps Kimi's tool names to actual RecursiveContextManager methods.
582
+ Enhances queries for semantic search tools.
583
+ """
584
+ # Enhance queries for search tools
585
+ if "search" in tool_name and "query" in args:
586
+ original_query = args["query"]
587
+ args["query"] = enhance_query_for_semantic_search(original_query)
588
+ print(f"🔍 Enhanced query: '{original_query}' → '{args['query']}'")
589
+
590
+ # Map tool names to actual methods
591
+ tool_map = {
592
+ "search_conversations": ctx.search_conversations,
593
+ "search_code": ctx.search_code,
594
+ "read_file": ctx.read_file,
595
+ "list_files": ctx.list_files,
596
+ "search_testament": ctx.search_testament,
597
+ }
598
+
599
+ if tool_name not in tool_map:
600
+ return f"Error: Unknown tool '{tool_name}'"
601
+
602
+ try:
603
+ result = tool_map[tool_name](**args)
604
+ return result
605
+ except Exception as e:
606
+ return f"Error executing {tool_name}: {e}"
607
+
608
+ def get_recent_context(history, n=5):
609
+ """
610
+ Get last N conversation turns for auto-context injection.
611
+
612
+ RATIONALE:
613
+ Always giving Kimi recent context reduces need for tool calls
614
+ and provides continuity across exchanges.
615
+
616
+ Gradio 6.0 format: [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
617
+ """
618
+ if not history or len(history) < 2:
619
+ return ""
620
+
621
+ # Get last N*2 messages (each turn = user + assistant)
622
+ recent = history[-(n*2):]
623
+
624
+ context_parts = []
625
+ for msg in recent:
626
+ role = msg.get("role", "unknown")
627
+ content = msg.get("content", "")
628
+ context_parts.append(f"{role}: {content[:200]}...") # Truncate long messages
629
+
630
+ return "Recent context:\n" + "\n".join(context_parts)
631
+
632
+ # Event handlers - Gradio 6.0 message format with MULTIMODAL support
633
+ def handle_submit(message, uploaded_file, history):
634
+ """
635
+ Handle message submission with multimodal support and translation layer.
636
+
637
+ CHANGELOG [2025-01-30 - Josh]
638
+ Phase 1: Translation layer for Kimi's tool calling
639
+ Phase 2: Multimodal file upload (images, PDFs, videos)
640
+
641
+ Kimi K2.5 is natively multimodal, so we can send:
642
+ - Images → Vision analysis
643
+ - PDFs → Document understanding
644
+ - Videos → Content analysis
645
+ - Code files → Review and integration
646
+
647
+ The translation layer:
648
+ 1. Parses Kimi's native tool call format
649
+ 2. Enhances queries for semantic search
650
+ 3. Executes tools via RecursiveContextManager
651
+ 4. Injects results + recent context back to Kimi
652
+ 5. Saves all conversations to ChromaDB for persistence
653
+ """
654
+ if not message.strip() and not uploaded_file:
655
+ return history, "", None # Clear file upload too
656
+
657
+ ctx = initialize_context()
658
+
659
+ # Process uploaded file if present
660
+ file_context = ""
661
+ if uploaded_file:
662
+ import os
663
+ file_path = uploaded_file
664
+ file_name = os.path.basename(file_path)
665
+ file_ext = os.path.splitext(file_name)[1].lower()
666
+
667
+ print(f"📎 Processing uploaded file: {file_name}")
668
+
669
+ # Handle different file types
670
+ if file_ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
671
+ # Image - Kimi will analyze via vision
672
+ file_context = f"\n\n[User uploaded image: {file_name}]"
673
+ # TODO: Add image to message content for Kimi's vision
674
+
675
+ elif file_ext == '.pdf':
676
+ # PDF - can extract text or let Kimi process
677
+ file_context = f"\n\n[User uploaded PDF: {file_name}]"
678
+ # TODO: Extract PDF text or send to Kimi
679
+
680
+ elif file_ext in ['.mp4', '.mov', '.avi']:
681
+ # Video - describe for Kimi
682
+ file_context = f"\n\n[User uploaded video: {file_name}]"
683
+ # TODO: Video frame extraction or description
684
+
685
+ elif file_ext in ['.txt', '.md', '.py', '.js', '.ts']:
686
+ # Text files - read and include
687
+ try:
688
+ with open(file_path, 'r') as f:
689
+ content = f.read()
690
+ file_context = f"\n\n[User uploaded {file_name}]:\n```{file_ext[1:]}\n{content}\n```"
691
+ except Exception as e:
692
+ file_context = f"\n\n[Error reading {file_name}: {e}]"
693
+
694
+ # Combine message with file context
695
+ full_message = message + file_context if file_context else message
696
+
697
+ # PHASE 1: Initial response from Kimi
698
+ response = chat(full_message, history)
699
+
700
+ # PHASE 2: Check for tool calls in Kimi's native format
701
+ tool_calls = parse_kimi_tool_call(response)
702
+
703
+ if tool_calls:
704
+ print(f"🔧 Detected {len(tool_calls)} tool call(s)")
705
+
706
+ # Execute all tool calls
707
+ tool_results = []
708
+ for tool_name, args in tool_calls:
709
+ print(f"🔧 Executing: {tool_name}({args})")
710
+ result = execute_tool(tool_name, args, ctx)
711
+ tool_results.append(f"Tool: {tool_name}\nResult: {result}")
712
+
713
+ # Inject tool results + recent context back to Kimi
714
+ context = get_recent_context(history, n=3)
715
+ tool_context = "\n\n".join(tool_results)
716
+
717
+ # Give Kimi the results and ask for final response
718
+ followup_message = f"{context}\n\nTool Results:\n{tool_context}\n\nBased on these results, please provide your response to the user."
719
+
720
+ # Get final response with tool results
721
+ final_response = chat(followup_message, history + [
722
+ {"role": "user", "content": full_message},
723
+ {"role": "assistant", "content": response}
724
+ ])
725
+
726
+ response = final_response
727
+
728
+ # Gradio 6.0 format: list of dicts with 'role' and 'content'
729
+ history.append({"role": "user", "content": full_message})
730
+ history.append({"role": "assistant", "content": response})
731
+
732
+ # PERSISTENCE: Save this conversation turn
733
+ # Turn ID = current history length (monotonic, unique)
734
+ turn_id = len(history) // 2 # Divide by 2 since each turn has user + assistant
735
+ try:
736
+ ctx.save_conversation_turn(full_message, response, turn_id)
737
+ except Exception as e:
738
+ print(f"⚠️ Failed to save conversation: {e}")
739
+
740
+ return history, "", None # Clear textbox AND file upload
741
+
742
+ submit.click(handle_submit, [msg, upload, chatbot], [chatbot, msg, upload])
743
+ msg.submit(handle_submit, [msg, upload, chatbot], [chatbot, msg, upload])
744
+ clear.click(lambda: ([], "", None), None, [chatbot, msg, upload], queue=False)
745
+ refresh_stats.click(get_stats, None, stats)
746
+
747
+ # Launch when run directly
748
+ if __name__ == "__main__":
749
+ print("🦞 Initializing Clawdbot...")
750
+ initialize_context()
751
+ print("✅ Context manager ready")
752
+ print("🚀 Launching Gradio interface...")
753
+ demo.launch(
754
+ server_name="0.0.0.0",
755
+ server_port=7860,
756
+ show_error=True
757
+ )
entrypoint.sh ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #
3
+ # Entrypoint script for Clawdbot
4
+ #
5
+ # CHANGELOG [2025-01-29 - Josh]
6
+ # Created to handle runtime repo cloning with authentication
7
+ #
8
+ # This script:
9
+ # 1. Clones E-T Systems repo if REPO_URL is provided
10
+ # 2. Uses GITHUB_TOKEN for authentication
11
+ # 3. Starts the Gradio app
12
+
13
+ set -e
14
+
15
+ echo "🦞 Clawdbot Entrypoint Starting..."
16
+
17
+ # Clone repository if URL provided
18
+ if [ -n "$REPO_URL" ]; then
19
+ echo "📦 Repository URL detected: $REPO_URL"
20
+
21
+ if [ -n "$GITHUB_TOKEN" ]; then
22
+ echo "🔑 GitHub token found, cloning with authentication..."
23
+ # Insert token into URL for authentication
24
+ AUTH_URL=$(echo "$REPO_URL" | sed "s|https://|https://${GITHUB_TOKEN}@|")
25
+ git clone "$AUTH_URL" /workspace/e-t-systems 2>&1 || echo "⚠️ Clone failed or repo already exists"
26
+ else
27
+ echo "⚠️ No GITHUB_TOKEN found, attempting public clone..."
28
+ git clone "$REPO_URL" /workspace/e-t-systems 2>&1 || echo "⚠️ Clone failed or repo already exists"
29
+ fi
30
+ else
31
+ echo "ℹ️ No REPO_URL provided, using demo repository"
32
+ fi
33
+
34
+ # Check what got cloned
35
+ echo "📂 Repository contents:"
36
+ ls -la /workspace/e-t-systems/ || echo "⚠️ Repository directory doesn't exist yet"
37
+
38
+ # Start the application
39
+ echo "🚀 Starting Gradio application..."
40
+ exec python app.py
recursive_context.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Recursive Context Manager for Clawdbot
3
+
4
+ CHANGELOG [2025-01-28 - Josh]
5
+ Implements MIT's Recursive Language Model technique for unlimited context.
6
+
7
+ REFERENCE: https://www.youtube.com/watch?v=huszaaJPjU8
8
+ "MIT basically solved unlimited context windows"
9
+
10
+ APPROACH:
11
+ Instead of cramming everything into context (hits limits) or summarizing
12
+ (lossy compression), we:
13
+
14
+ 1. Store entire codebase in searchable environment
15
+ 2. Give model TOOLS to query what it needs
16
+ 3. Model recursively retrieves relevant pieces
17
+ 4. No summarization loss - full fidelity access
18
+
19
+ This is like RAG, but IN-ENVIRONMENT with the model actively deciding
20
+ what context it needs rather than us guessing upfront.
21
+
22
+ EXAMPLE FLOW:
23
+ User: "How does Genesis handle surprise?"
24
+ Model: search_code("Genesis surprise detection")
25
+ → Finds: genesis/substrate.py, genesis/attention.py
26
+ Model: read_file("genesis/substrate.py", lines 145-167)
27
+ → Gets actual implementation
28
+ Model: search_testament("surprise detection rationale")
29
+ → Gets design decision
30
+ Model: Synthesizes answer from retrieved pieces
31
+
32
+ NO CONTEXT WINDOW LIMIT - just selective retrieval.
33
+ """
34
+
35
+ from pathlib import Path
36
+ from typing import List, Dict, Optional, Tuple
37
+ import chromadb
38
+ from chromadb.config import Settings
39
+ import hashlib
40
+
41
+
42
+ class RecursiveContextManager:
43
+ """
44
+ Manages unlimited context via recursive retrieval.
45
+
46
+ The model has TOOLS to search and read the codebase selectively,
47
+ rather than loading everything upfront.
48
+ """
49
+
50
+ def __init__(self, repo_path: str):
51
+ """
52
+ Initialize context manager for a repository.
53
+
54
+ Args:
55
+ repo_path: Path to the code repository
56
+ """
57
+ self.repo_path = Path(repo_path)
58
+
59
+ # Initialize ChromaDB for semantic search
60
+ # Using persistent storage so we don't re-index every restart
61
+ self.chroma_client = chromadb.PersistentClient(
62
+ path="/workspace/chroma_db",
63
+ settings=Settings(
64
+ anonymized_telemetry=False,
65
+ allow_reset=True
66
+ )
67
+ )
68
+
69
+ # Create or get CODEBASE collection
70
+ collection_name = self._get_collection_name()
71
+ try:
72
+ self.collection = self.chroma_client.get_collection(collection_name)
73
+ print(f"📚 Loaded existing index: {self.collection.count()} files")
74
+ except:
75
+ self.collection = self.chroma_client.create_collection(
76
+ name=collection_name,
77
+ metadata={"description": "E-T Systems codebase"}
78
+ )
79
+ print(f"🆕 Created new collection: {collection_name}")
80
+ self._index_codebase()
81
+
82
+ # Create or get CONVERSATION collection for persistence
83
+ # CHANGELOG [2025-01-30 - Josh]: Added conversation persistence
84
+ # Implements full MIT recursive technique - chat history is searchable context
85
+ conversations_name = f"conversations_{self._get_collection_name().split('_')[1]}"
86
+ try:
87
+ self.conversations = self.chroma_client.get_collection(conversations_name)
88
+ print(f"💬 Loaded conversation history: {self.conversations.count()} exchanges")
89
+ except:
90
+ self.conversations = self.chroma_client.create_collection(
91
+ name=conversations_name,
92
+ metadata={"description": "Clawdbot conversation history"}
93
+ )
94
+ print(f"🆕 Created conversation collection: {conversations_name}")
95
+
96
+ def _get_collection_name(self) -> str:
97
+ """Generate unique collection name based on repo path."""
98
+ path_hash = hashlib.md5(str(self.repo_path).encode()).hexdigest()[:8]
99
+ return f"codebase_{path_hash}"
100
+
101
+ def _index_codebase(self):
102
+ """
103
+ Index all code files for semantic search.
104
+
105
+ This creates the "environment" that the model can search through.
106
+ We index with metadata so search results include file paths.
107
+ """
108
+ print(f"📂 Indexing codebase at {self.repo_path}...")
109
+
110
+ # File types to index
111
+ code_extensions = {'.py', '.js', '.ts', '.tsx', '.jsx', '.md', '.txt', '.json', '.yaml', '.yml'}
112
+
113
+ # Skip these directories
114
+ skip_dirs = {'node_modules', '.git', '__pycache__', 'venv', 'env', '.venv', 'dist', 'build'}
115
+
116
+ documents = []
117
+ metadatas = []
118
+ ids = []
119
+
120
+ for file_path in self.repo_path.rglob('*'):
121
+ # Skip directories and non-code files
122
+ if file_path.is_dir():
123
+ continue
124
+ if any(skip in file_path.parts for skip in skip_dirs):
125
+ continue
126
+ if file_path.suffix not in code_extensions:
127
+ continue
128
+
129
+ try:
130
+ content = file_path.read_text(encoding='utf-8', errors='ignore')
131
+
132
+ # Don't index empty files or massive files
133
+ if not content.strip() or len(content) > 100000:
134
+ continue
135
+
136
+ relative_path = str(file_path.relative_to(self.repo_path))
137
+
138
+ documents.append(content)
139
+ metadatas.append({
140
+ "path": relative_path,
141
+ "type": file_path.suffix[1:], # Remove leading dot
142
+ "size": len(content)
143
+ })
144
+ ids.append(relative_path)
145
+
146
+ except Exception as e:
147
+ print(f"⚠️ Skipping {file_path.name}: {e}")
148
+ continue
149
+
150
+ if documents:
151
+ # Add to collection in batches
152
+ batch_size = 100
153
+ for i in range(0, len(documents), batch_size):
154
+ batch_docs = documents[i:i+batch_size]
155
+ batch_meta = metadatas[i:i+batch_size]
156
+ batch_ids = ids[i:i+batch_size]
157
+
158
+ self.collection.add(
159
+ documents=batch_docs,
160
+ metadatas=batch_meta,
161
+ ids=batch_ids
162
+ )
163
+
164
+ print(f"✅ Indexed {len(documents)} files")
165
+ else:
166
+ print("⚠️ No files found to index")
167
+
168
+ def search_code(self, query: str, n_results: int = 5) -> List[Dict]:
169
+ """
170
+ Search codebase semantically.
171
+
172
+ This is a TOOL available to the model for recursive retrieval.
173
+ Model can search for concepts without knowing exact file names.
174
+
175
+ Args:
176
+ query: What to search for (e.g. "surprise detection", "vector embedding")
177
+ n_results: How many results to return
178
+
179
+ Returns:
180
+ List of dicts with {file, snippet, relevance}
181
+ """
182
+ if self.collection.count() == 0:
183
+ return [{"error": "No files indexed yet"}]
184
+
185
+ results = self.collection.query(
186
+ query_texts=[query],
187
+ n_results=min(n_results, self.collection.count())
188
+ )
189
+
190
+ # Format results for the model
191
+ formatted = []
192
+ for i in range(len(results['documents'][0])):
193
+ # Truncate document to first 500 chars for search results
194
+ # Model can read_file() if it wants the full content
195
+ snippet = results['documents'][0][i][:500]
196
+ if len(results['documents'][0][i]) > 500:
197
+ snippet += "... [truncated, use read_file to see more]"
198
+
199
+ formatted.append({
200
+ "file": results['metadatas'][0][i]['path'],
201
+ "snippet": snippet,
202
+ "relevance": round(1 - results['distances'][0][i], 3),
203
+ "type": results['metadatas'][0][i]['type']
204
+ })
205
+
206
+ return formatted
207
+
208
+ def read_file(self, path: str, lines: Optional[Tuple[int, int]] = None) -> str:
209
+ """
210
+ Read a specific file or line range.
211
+
212
+ This is a TOOL available to the model.
213
+ After searching, model can read full files as needed.
214
+
215
+ Args:
216
+ path: Relative path to file
217
+ lines: Optional (start, end) line numbers (1-indexed, inclusive)
218
+
219
+ Returns:
220
+ File content or specified lines
221
+ """
222
+ full_path = self.repo_path / path
223
+
224
+ if not full_path.exists():
225
+ return f"Error: File not found: {path}"
226
+
227
+ if not full_path.is_relative_to(self.repo_path):
228
+ return "Error: Path outside repository"
229
+
230
+ try:
231
+ content = full_path.read_text(encoding='utf-8', errors='ignore')
232
+
233
+ if lines:
234
+ start, end = lines
235
+ content_lines = content.split('\n')
236
+ # Adjust for 1-indexed
237
+ selected_lines = content_lines[start-1:end]
238
+ return '\n'.join(selected_lines)
239
+
240
+ return content
241
+
242
+ except Exception as e:
243
+ return f"Error reading file: {str(e)}"
244
+
245
+ def search_testament(self, query: str) -> str:
246
+ """
247
+ Search architectural decisions in Testament.
248
+
249
+ This is a TOOL available to the model.
250
+ Helps model understand design rationale.
251
+
252
+ Args:
253
+ query: What decision to look for
254
+
255
+ Returns:
256
+ Relevant Testament sections
257
+ """
258
+ testament_path = self.repo_path / "TESTAMENT.md"
259
+
260
+ if not testament_path.exists():
261
+ return "Testament not found. No architectural decisions recorded yet."
262
+
263
+ try:
264
+ content = testament_path.read_text(encoding='utf-8')
265
+
266
+ # Split into sections (marked by ## headers)
267
+ sections = content.split('\n## ')
268
+
269
+ # Simple relevance: sections that contain query terms
270
+ query_lower = query.lower()
271
+ relevant = []
272
+
273
+ for section in sections:
274
+ if query_lower in section.lower():
275
+ # Include section with header
276
+ if not section.startswith('#'):
277
+ section = '## ' + section
278
+ relevant.append(section)
279
+
280
+ if relevant:
281
+ return '\n\n'.join(relevant)
282
+ else:
283
+ return f"No Testament entries found matching '{query}'"
284
+
285
+ except Exception as e:
286
+ return f"Error searching Testament: {str(e)}"
287
+
288
+ def list_files(self, directory: str = ".") -> List[str]:
289
+ """
290
+ List files in a directory.
291
+
292
+ This is a TOOL available to the model.
293
+ Helps model explore repository structure.
294
+
295
+ Args:
296
+ directory: Directory to list (relative path)
297
+
298
+ Returns:
299
+ List of file/directory names
300
+ """
301
+ dir_path = self.repo_path / directory
302
+
303
+ if not dir_path.exists():
304
+ return [f"Error: Directory not found: {directory}"]
305
+
306
+ if not dir_path.is_relative_to(self.repo_path):
307
+ return ["Error: Path outside repository"]
308
+
309
+ try:
310
+ items = []
311
+ for item in sorted(dir_path.iterdir()):
312
+ # Skip hidden and system directories
313
+ if item.name.startswith('.'):
314
+ continue
315
+ if item.name in {'node_modules', '__pycache__', 'venv'}:
316
+ continue
317
+
318
+ # Mark directories with /
319
+ if item.is_dir():
320
+ items.append(f"{item.name}/")
321
+ else:
322
+ items.append(item.name)
323
+
324
+ return items
325
+
326
+ except Exception as e:
327
+ return [f"Error listing directory: {str(e)}"]
328
+
329
+ def save_conversation_turn(self, user_message: str, assistant_message: str, turn_id: int):
330
+ """
331
+ Save a conversation turn to persistent storage.
332
+
333
+ CHANGELOG [2025-01-30 - Josh]
334
+ Implements MIT recursive technique for conversations.
335
+ Chat history becomes searchable context that persists across sessions.
336
+
337
+ Args:
338
+ user_message: What the user said
339
+ assistant_message: What Clawdbot responded
340
+ turn_id: Unique ID for this turn (timestamp-based)
341
+ """
342
+ import time
343
+
344
+ # Create a combined document for semantic search
345
+ combined = f"USER: {user_message}\n\nASSISTANT: {assistant_message}"
346
+
347
+ # Save with metadata
348
+ self.conversations.add(
349
+ documents=[combined],
350
+ metadatas=[{
351
+ "user": user_message[:500], # Truncate for metadata
352
+ "assistant": assistant_message[:500],
353
+ "timestamp": int(time.time()),
354
+ "turn": turn_id
355
+ }],
356
+ ids=[f"turn_{turn_id}"]
357
+ )
358
+
359
+ print(f"💾 Saved conversation turn {turn_id}")
360
+
361
+ def search_conversations(self, query: str, n_results: int = 5) -> List[Dict]:
362
+ """
363
+ Search past conversations for relevant context.
364
+
365
+ This enables TRUE unlimited context - Clawdbot can remember
366
+ everything ever discussed by searching its own conversation history.
367
+
368
+ Args:
369
+ query: What to search for in past conversations
370
+ n_results: How many results to return
371
+
372
+ Returns:
373
+ List of past conversation turns with user/assistant messages
374
+ """
375
+ if self.conversations.count() == 0:
376
+ return []
377
+
378
+ results = self.conversations.query(
379
+ query_texts=[query],
380
+ n_results=min(n_results, self.conversations.count())
381
+ )
382
+
383
+ formatted = []
384
+ for i, (doc, metadata) in enumerate(zip(results['documents'][0], results['metadatas'][0])):
385
+ formatted.append({
386
+ "turn": metadata.get("turn", "unknown"),
387
+ "user": metadata.get("user", ""),
388
+ "assistant": metadata.get("assistant", ""),
389
+ "full_text": doc,
390
+ "relevance": i + 1 # Lower is more relevant
391
+ })
392
+
393
+ return formatted
394
+
395
+ def get_conversation_count(self) -> int:
396
+ """Get total number of saved conversation turns."""
397
+ return self.conversations.count()
398
+
399
+ def get_stats(self) -> Dict:
400
+ """
401
+ Get statistics about indexed codebase.
402
+
403
+ Returns:
404
+ Dict with file counts, sizes, etc.
405
+ """
406
+ return {
407
+ "total_files": self.collection.count(),
408
+ "repo_path": str(self.repo_path),
409
+ "collection_name": self.collection.name
410
+ }
requirements.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python Dependencies for Clawdbot Dev Assistant
2
+ #
3
+ # CHANGELOG [2025-01-28 - Josh]
4
+ # Core dependencies for recursive context + HF inference
5
+
6
+ # Gradio for web interface (5.0+ required for type="messages" format)
7
+ gradio>=5.0.0
8
+ pytz>=2023.3
9
+
10
+ # HuggingFace for model inference
11
+ huggingface-hub>=0.20.0
12
+
13
+ # ChromaDB for vector search (recursive context)
14
+ chromadb>=0.4.0
15
+ pydantic>=2.0.0
16
+ pydantic-settings>=2.0.0
17
+
18
+ # Additional utilities
19
+ requests>=2.31.0
20
+ gitpython>=3.1.0
21
+
22
+ # Performance
23
+ numpy>=1.24.0