Upload 5 files
Browse files- Dockerfile +72 -0
- app.py +757 -0
- entrypoint.sh +40 -0
- recursive_context.py +410 -0
- 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
|