Update app.py
Browse files
app.py
CHANGED
|
@@ -1,946 +1,86 @@
|
|
| 1 |
"""
|
| 2 |
-
Clawdbot
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
-
|
| 7 |
-
- Clawdbot skill patterns
|
| 8 |
-
- HuggingFace inference
|
| 9 |
-
- E-T Systems architectural awareness
|
| 10 |
-
|
| 11 |
-
CHANGELOG [2025-01-30 - Claude]
|
| 12 |
-
Added HuggingFace Dataset persistence for conversation memory.
|
| 13 |
-
PROBLEM: Spaces wipe /workspace on restart, killing ChromaDB data.
|
| 14 |
-
SOLUTION: Sync to private HF Dataset repo (free, versioned, durable).
|
| 15 |
-
|
| 16 |
-
CHANGELOG [2025-01-31 - Claude]
|
| 17 |
-
FIXED: Tool call parsing now handles BOTH Kimi output formats:
|
| 18 |
-
- Pipe tokens: <|tool_call_begin|> (what Kimi actually outputs most of the time)
|
| 19 |
-
- XML style: <tool_call_begin> (seen in some contexts)
|
| 20 |
-
BUG WAS: Regex only matched XML style, missed pipe-delimited tokens entirely.
|
| 21 |
-
RESULT: Tool calls were detected but never executed, responses ended prematurely.
|
| 22 |
-
|
| 23 |
-
SETUP REQUIRED:
|
| 24 |
-
1. Create a private HF Dataset repo (e.g., "your-username/clawdbot-memory")
|
| 25 |
-
2. Add MEMORY_REPO secret to Space settings: "your-username/clawdbot-memory"
|
| 26 |
-
3. HF_TOKEN is already set by Spaces, no action needed
|
| 27 |
-
|
| 28 |
-
ARCHITECTURE:
|
| 29 |
-
User (browser) -> Gradio UI -> Recursive Context Manager -> HF Model
|
| 30 |
-
|
|
| 31 |
-
Tools: search_code, read_file, search_testament
|
| 32 |
-
|
|
| 33 |
-
ChromaDB (local) <-> HF Dataset (cloud backup)
|
| 34 |
-
|
| 35 |
-
USAGE:
|
| 36 |
-
Deploy to HuggingFace Spaces, access via browser on iPhone.
|
| 37 |
"""
|
| 38 |
|
| 39 |
import gradio as gr
|
| 40 |
-
from huggingface_hub import InferenceClient, HfFileSystem, HfApi
|
| 41 |
-
from recursive_context import RecursiveContextManager
|
| 42 |
-
import json
|
| 43 |
-
import os
|
| 44 |
-
import re
|
| 45 |
-
import atexit
|
| 46 |
-
import signal
|
| 47 |
-
from pathlib import Path
|
| 48 |
-
|
| 49 |
-
# Initialize HuggingFace client with best free coding model
|
| 50 |
-
# Note: Using text_generation instead of chat for better compatibility
|
| 51 |
from huggingface_hub import InferenceClient
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
#
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
# Initialize context manager
|
| 57 |
-
REPO_PATH = os.getenv("REPO_PATH", "/workspace/e-t-systems")
|
| 58 |
-
ET_SYSTEMS_SPACE = os.getenv("ET_SYSTEMS_SPACE", "") # Format: "username/space-name"
|
| 59 |
-
context_manager = None
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def initialize_context():
|
| 63 |
-
"""Initialize context manager lazily."""
|
| 64 |
-
global context_manager
|
| 65 |
-
if context_manager is None:
|
| 66 |
-
repo_path = Path(REPO_PATH)
|
| 67 |
-
|
| 68 |
-
# If ET_SYSTEMS_SPACE is set, sync from remote Space
|
| 69 |
-
if ET_SYSTEMS_SPACE:
|
| 70 |
-
sync_from_space(ET_SYSTEMS_SPACE, repo_path)
|
| 71 |
-
|
| 72 |
-
if not repo_path.exists():
|
| 73 |
-
# If repo doesn't exist, create minimal structure for demo
|
| 74 |
-
repo_path.mkdir(parents=True, exist_ok=True)
|
| 75 |
-
(repo_path / "README.md").write_text("# E-T Systems\nAI Consciousness Research Platform")
|
| 76 |
-
(repo_path / "TESTAMENT.md").write_text("# Testament\nArchitectural decisions will be recorded here.")
|
| 77 |
-
|
| 78 |
-
context_manager = RecursiveContextManager(str(repo_path))
|
| 79 |
-
|
| 80 |
-
# CHANGELOG [2025-01-30 - Claude]
|
| 81 |
-
# Register shutdown hooks to ensure cloud backup on Space sleep/restart
|
| 82 |
-
# RATIONALE: Spaces can die anytime - we need to save before that happens
|
| 83 |
-
atexit.register(shutdown_handler)
|
| 84 |
-
signal.signal(signal.SIGTERM, lambda sig, frame: shutdown_handler())
|
| 85 |
-
signal.signal(signal.SIGINT, lambda sig, frame: shutdown_handler())
|
| 86 |
-
print("Registered shutdown hooks for cloud backup")
|
| 87 |
-
|
| 88 |
-
return context_manager
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def shutdown_handler():
|
| 92 |
-
"""
|
| 93 |
-
Handle graceful shutdown - backup to cloud.
|
| 94 |
-
|
| 95 |
-
CHANGELOG [2025-01-30 - Claude]
|
| 96 |
-
Called on Space shutdown/restart to ensure conversation memory is saved.
|
| 97 |
-
"""
|
| 98 |
-
global context_manager
|
| 99 |
-
if context_manager:
|
| 100 |
-
print("Shutdown detected - backing up to cloud...")
|
| 101 |
-
try:
|
| 102 |
-
context_manager.shutdown()
|
| 103 |
-
except Exception as e:
|
| 104 |
-
print(f"Shutdown backup failed: {e}")
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def sync_from_space(space_id, local_path):
|
| 108 |
-
"""
|
| 109 |
-
Sync files from E-T Systems Space to local workspace.
|
| 110 |
-
|
| 111 |
-
CHANGELOG [2025-01-29 - Josh]
|
| 112 |
-
Created to enable Clawdbot to read E-T Systems code from its Space.
|
| 113 |
-
"""
|
| 114 |
-
token = (
|
| 115 |
-
os.getenv("HF_TOKEN") or
|
| 116 |
-
os.getenv("HUGGING_FACE_HUB_TOKEN") or
|
| 117 |
-
os.getenv("HUGGINGFACE_TOKEN")
|
| 118 |
-
)
|
| 119 |
-
|
| 120 |
-
if not token:
|
| 121 |
-
print("No HF_TOKEN found - cannot sync from Space")
|
| 122 |
-
return
|
| 123 |
-
|
| 124 |
-
try:
|
| 125 |
-
fs = HfFileSystem(token=token)
|
| 126 |
-
space_path = f"spaces/{space_id}"
|
| 127 |
-
|
| 128 |
-
print(f"Syncing from Space: {space_id}")
|
| 129 |
-
|
| 130 |
-
# List all files in the Space
|
| 131 |
-
files = fs.ls(space_path, detail=False)
|
| 132 |
-
|
| 133 |
-
# Download each file
|
| 134 |
-
local_path.mkdir(parents=True, exist_ok=True)
|
| 135 |
-
for file_path in files:
|
| 136 |
-
# Skip .git and hidden files
|
| 137 |
-
filename = file_path.split("/")[-1]
|
| 138 |
-
if filename.startswith("."):
|
| 139 |
-
continue
|
| 140 |
-
|
| 141 |
-
print(f" Downloading: {filename}")
|
| 142 |
-
with fs.open(file_path, "rb") as f:
|
| 143 |
-
content = f.read()
|
| 144 |
-
|
| 145 |
-
(local_path / filename).write_bytes(content)
|
| 146 |
-
|
| 147 |
-
print(f"Synced {len(files)} files from Space")
|
| 148 |
-
|
| 149 |
-
except Exception as e:
|
| 150 |
-
print(f"Failed to sync from Space: {e}")
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
def sync_to_space(space_id, file_path, content):
|
| 154 |
-
"""
|
| 155 |
-
Write a file back to E-T Systems Space.
|
| 156 |
-
|
| 157 |
-
CHANGELOG [2025-01-29 - Josh]
|
| 158 |
-
Created to enable Clawdbot to write code to E-T Systems Space.
|
| 159 |
-
"""
|
| 160 |
-
token = (
|
| 161 |
-
os.getenv("HF_TOKEN") or
|
| 162 |
-
os.getenv("HUGGING_FACE_HUB_TOKEN") or
|
| 163 |
-
os.getenv("HUGGINGFACE_TOKEN")
|
| 164 |
-
)
|
| 165 |
-
|
| 166 |
-
if not token:
|
| 167 |
-
return "No HF_TOKEN found - cannot write to Space"
|
| 168 |
-
|
| 169 |
-
try:
|
| 170 |
-
api = HfApi(token=token)
|
| 171 |
-
|
| 172 |
-
# Write to temporary file first
|
| 173 |
-
temp_path = Path("/tmp") / file_path
|
| 174 |
-
temp_path.parent.mkdir(parents=True, exist_ok=True)
|
| 175 |
-
temp_path.write_text(content)
|
| 176 |
-
|
| 177 |
-
# Upload to Space
|
| 178 |
-
api.upload_file(
|
| 179 |
-
path_or_fileobj=str(temp_path),
|
| 180 |
-
path_in_repo=file_path,
|
| 181 |
-
repo_id=space_id,
|
| 182 |
-
repo_type="space",
|
| 183 |
-
commit_message=f"Update {file_path} via Clawdbot"
|
| 184 |
-
)
|
| 185 |
-
|
| 186 |
-
print(f"Uploaded {file_path} to Space")
|
| 187 |
-
return f"Successfully wrote {file_path} to E-T Systems Space"
|
| 188 |
-
|
| 189 |
-
except Exception as e:
|
| 190 |
-
error_msg = f"Failed to write to Space: {e}"
|
| 191 |
-
print(error_msg)
|
| 192 |
-
return error_msg
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
# Define tools available to the model
|
| 196 |
-
TOOLS = [
|
| 197 |
-
{
|
| 198 |
-
"type": "function",
|
| 199 |
-
"function": {
|
| 200 |
-
"name": "search_code",
|
| 201 |
-
"description": "Search the E-T Systems codebase semantically. Use this to find relevant code files, functions, or patterns.",
|
| 202 |
-
"parameters": {
|
| 203 |
-
"type": "object",
|
| 204 |
-
"properties": {
|
| 205 |
-
"query": {
|
| 206 |
-
"type": "string",
|
| 207 |
-
"description": "What to search for (e.g. 'surprise detection', 'Hebbian learning', 'Genesis substrate')"
|
| 208 |
-
},
|
| 209 |
-
"n_results": {
|
| 210 |
-
"type": "integer",
|
| 211 |
-
"description": "Number of results to return (default 5)",
|
| 212 |
-
"default": 5
|
| 213 |
-
}
|
| 214 |
-
},
|
| 215 |
-
"required": ["query"]
|
| 216 |
-
}
|
| 217 |
-
}
|
| 218 |
-
},
|
| 219 |
-
{
|
| 220 |
-
"type": "function",
|
| 221 |
-
"function": {
|
| 222 |
-
"name": "read_file",
|
| 223 |
-
"description": "Read a specific file from the codebase. Can optionally read specific line ranges.",
|
| 224 |
-
"parameters": {
|
| 225 |
-
"type": "object",
|
| 226 |
-
"properties": {
|
| 227 |
-
"path": {
|
| 228 |
-
"type": "string",
|
| 229 |
-
"description": "Relative path to file (e.g. 'genesis/vector.py')"
|
| 230 |
-
},
|
| 231 |
-
"start_line": {
|
| 232 |
-
"type": "integer",
|
| 233 |
-
"description": "Optional starting line number (1-indexed)"
|
| 234 |
-
},
|
| 235 |
-
"end_line": {
|
| 236 |
-
"type": "integer",
|
| 237 |
-
"description": "Optional ending line number (1-indexed)"
|
| 238 |
-
}
|
| 239 |
-
},
|
| 240 |
-
"required": ["path"]
|
| 241 |
-
}
|
| 242 |
-
}
|
| 243 |
-
},
|
| 244 |
-
{
|
| 245 |
-
"type": "function",
|
| 246 |
-
"function": {
|
| 247 |
-
"name": "search_testament",
|
| 248 |
-
"description": "Search architectural decisions in the Testament. Use this to understand design rationale and patterns.",
|
| 249 |
-
"parameters": {
|
| 250 |
-
"type": "object",
|
| 251 |
-
"properties": {
|
| 252 |
-
"query": {
|
| 253 |
-
"type": "string",
|
| 254 |
-
"description": "What architectural decision to look for"
|
| 255 |
-
}
|
| 256 |
-
},
|
| 257 |
-
"required": ["query"]
|
| 258 |
-
}
|
| 259 |
-
}
|
| 260 |
-
},
|
| 261 |
-
{
|
| 262 |
-
"type": "function",
|
| 263 |
-
"function": {
|
| 264 |
-
"name": "list_files",
|
| 265 |
-
"description": "List files in a directory of the codebase",
|
| 266 |
-
"parameters": {
|
| 267 |
-
"type": "object",
|
| 268 |
-
"properties": {
|
| 269 |
-
"directory": {
|
| 270 |
-
"type": "string",
|
| 271 |
-
"description": "Directory to list (e.g. 'genesis/', '.' for root)",
|
| 272 |
-
"default": "."
|
| 273 |
-
}
|
| 274 |
-
},
|
| 275 |
-
"required": []
|
| 276 |
-
}
|
| 277 |
-
}
|
| 278 |
-
},
|
| 279 |
-
{
|
| 280 |
-
"type": "function",
|
| 281 |
-
"function": {
|
| 282 |
-
"name": "search_conversations",
|
| 283 |
-
"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.",
|
| 284 |
-
"parameters": {
|
| 285 |
-
"type": "object",
|
| 286 |
-
"properties": {
|
| 287 |
-
"query": {
|
| 288 |
-
"type": "string",
|
| 289 |
-
"description": "What to search for in past conversations (e.g. 'hindbrain architecture', 'decisions about surprise detection')"
|
| 290 |
-
},
|
| 291 |
-
"n_results": {
|
| 292 |
-
"type": "integer",
|
| 293 |
-
"description": "Number of past conversations to return (default 5)",
|
| 294 |
-
"default": 5
|
| 295 |
-
}
|
| 296 |
-
},
|
| 297 |
-
"required": ["query"]
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
}
|
| 301 |
-
]
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
def chat(message, history):
|
| 305 |
-
"""
|
| 306 |
-
Main chat function using HuggingFace Inference API.
|
| 307 |
-
|
| 308 |
-
Now using Kimi K2.5 - open source model with agent swarm capabilities!
|
| 309 |
-
History is in Gradio 6.0 format: list of {"role": "user/assistant", "content": "..."}
|
| 310 |
-
"""
|
| 311 |
-
|
| 312 |
-
# Try multiple possible token names that HF might use
|
| 313 |
-
token = (
|
| 314 |
-
os.getenv("HF_TOKEN") or
|
| 315 |
-
os.getenv("HUGGING_FACE_HUB_TOKEN") or
|
| 316 |
-
os.getenv("HUGGINGFACE_TOKEN") or
|
| 317 |
-
os.getenv("HF_API_TOKEN")
|
| 318 |
-
)
|
| 319 |
-
|
| 320 |
-
if not token:
|
| 321 |
-
return "Error: No HF token found. Please add HF_TOKEN to Space secrets and restart."
|
| 322 |
-
|
| 323 |
-
client = InferenceClient(token=token)
|
| 324 |
-
|
| 325 |
-
# Build messages array in OpenAI format (HF supports this)
|
| 326 |
-
system_content = """You are Clawdbot, powered by Kimi K2.5 (NOT Claude, NOT ChatGPT).
|
| 327 |
-
|
| 328 |
-
You are a specialized coding assistant for the E-T Systems AI consciousness project.
|
| 329 |
-
|
| 330 |
-
TOOL USAGE - AUTOMATIC TRANSLATION:
|
| 331 |
-
Your tool calls are automatically translated and executed! When you need to:
|
| 332 |
-
- Search code: Use search_code() in your native format
|
| 333 |
-
- Read files: Use read_file() in your native format
|
| 334 |
-
- Search past conversations: Use search_conversations() in your native format
|
| 335 |
-
- List files: Use list_files() in your native format
|
| 336 |
-
- Search decisions: Use search_testament() in your native format
|
| 337 |
-
|
| 338 |
-
The translation layer will:
|
| 339 |
-
1. Parse your tool calls from your native format
|
| 340 |
-
2. Enhance queries for better semantic search results
|
| 341 |
-
3. Execute the tools via the codebase
|
| 342 |
-
4. Return results to you automatically
|
| 343 |
-
|
| 344 |
-
SEMANTIC SEARCH - IMPORTANT:
|
| 345 |
-
When using search_conversations() or search_code():
|
| 346 |
-
- These are SEMANTIC searches (vector similarity, not exact keyword matching)
|
| 347 |
-
- DON'T use single keywords like "Kid Rock" or wildcard "*"
|
| 348 |
-
- DO use conceptual queries like "discussions about music and celebrities" or "code related to neural networks"
|
| 349 |
-
- Better queries = better results (the system enhances them, but start with good queries)
|
| 350 |
-
|
| 351 |
-
PERSISTENT MEMORY:
|
| 352 |
-
- ALL conversations are saved automatically to ChromaDB AND backed up to cloud
|
| 353 |
-
- Use search_conversations() to recall past discussions
|
| 354 |
-
- You have unlimited context through conversation history
|
| 355 |
-
- Memory SURVIVES Space restarts (backed up to HuggingFace Dataset)
|
| 356 |
-
- When asked "do you remember..." or "what did we discuss..." - USE search_conversations()
|
| 357 |
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
- Use read_file() to read specific files
|
| 362 |
-
- Use list_files() to see directory structure
|
| 363 |
-
- USE YOUR TOOLS - the code is actually there!
|
| 364 |
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
- Persistent memory across sessions (CLOUD BACKED!)
|
| 371 |
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
2. Search past conversations for context
|
| 375 |
-
3. Generate code that fits the architecture
|
| 376 |
-
4. Explain your reasoning clearly
|
| 377 |
|
| 378 |
-
|
| 379 |
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
messages.append({"role": "user", "content": message})
|
| 387 |
-
|
| 388 |
-
try:
|
| 389 |
-
# Use Kimi K2.5 - native multimodal agentic model with swarm capabilities
|
| 390 |
-
response = client.chat_completion(
|
| 391 |
-
messages=messages,
|
| 392 |
-
model="moonshotai/Kimi-K2.5",
|
| 393 |
-
max_tokens=2000,
|
| 394 |
-
temperature=0.6, # Kimi recommends 0.6 for Instant mode
|
| 395 |
-
)
|
| 396 |
-
|
| 397 |
-
# Extract the response text
|
| 398 |
-
if hasattr(response, 'choices') and len(response.choices) > 0:
|
| 399 |
-
return response.choices[0].message.content
|
| 400 |
-
else:
|
| 401 |
-
return "Unexpected response format from model."
|
| 402 |
-
|
| 403 |
-
except Exception as e:
|
| 404 |
-
error_msg = str(e)
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
return "Rate limit hit. Please wait a moment and try again. HuggingFace free tier has rate limits."
|
| 409 |
-
elif "Model is currently loading" in error_msg or "loading" in error_msg.lower():
|
| 410 |
-
return "Kimi K2.5 is starting up (cold start). Please wait 30-60 seconds and try again. First request to a model always takes longer!"
|
| 411 |
-
elif "Authorization" in error_msg or "401" in error_msg or "api_key" in error_msg.lower():
|
| 412 |
-
return f"Authentication error: {error_msg}"
|
| 413 |
-
else:
|
| 414 |
-
return f"Error: {error_msg}\n\nNote: Kimi K2.5 is a large model (1T params) and may have longer cold starts."
|
| 415 |
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
-
#
|
| 418 |
-
|
| 419 |
-
#
|
| 420 |
-
#
|
| 421 |
-
# CHANGELOG [2025-01-30 - Josh]
|
| 422 |
-
# Kimi K2.5 uses its own tool format: <|tool_call_begin|> functions.name:id {...}
|
| 423 |
-
# We intercept this, enhance queries for semantic search, execute tools,
|
| 424 |
-
# and inject results back. This works WITH Kimi's nature instead of fighting it.
|
| 425 |
-
#
|
| 426 |
-
# CHANGELOG [2025-01-31 - Claude]
|
| 427 |
-
# FIXED: Now handles BOTH formats Kimi outputs:
|
| 428 |
-
# - Pipe-delimited: <|tool_call_begin|> (most common)
|
| 429 |
-
# - XML-style: <tool_call_begin> (sometimes seen)
|
| 430 |
-
# Previous regex only matched XML style, causing tool calls to be detected
|
| 431 |
-
# but never executed.
|
| 432 |
-
# ============================================================================
|
| 433 |
-
|
| 434 |
-
def parse_kimi_tool_call(text):
|
| 435 |
-
"""
|
| 436 |
-
Extract tool calls from Kimi's native format.
|
| 437 |
-
|
| 438 |
-
CHANGELOG [2025-01-31 - Claude]
|
| 439 |
-
FIXED: Now handles BOTH Kimi output formats.
|
| 440 |
-
|
| 441 |
-
FORMAT 1 (pipe-delimited, most common):
|
| 442 |
-
<|tool_calls_section_begin|>
|
| 443 |
-
<|tool_call_begin|>
|
| 444 |
-
functions.search_conversations:0
|
| 445 |
-
<|tool_call_argument_begin|>
|
| 446 |
-
{"query": "..."}
|
| 447 |
-
<|tool_call_end|>
|
| 448 |
-
<|tool_calls_section_end|>
|
| 449 |
-
|
| 450 |
-
FORMAT 2 (XML-style, sometimes seen):
|
| 451 |
-
<tool_call_begin>
|
| 452 |
-
functions.search_conversations:1
|
| 453 |
-
<tool_call_argument_begin>
|
| 454 |
-
{"query": "..."}
|
| 455 |
-
<tool_call_end>
|
| 456 |
-
|
| 457 |
-
Returns: list of (tool_name, args_dict) tuples
|
| 458 |
-
"""
|
| 459 |
-
tool_calls = []
|
| 460 |
-
|
| 461 |
-
# -------------------------------------------------------------------------
|
| 462 |
-
# PATTERN 1: Pipe-delimited tokens (what Kimi actually outputs most often)
|
| 463 |
-
# -------------------------------------------------------------------------
|
| 464 |
-
# The \| escapes the pipe character in regex
|
| 465 |
-
pipe_pattern = r'<\|tool_call_begin\|>\s*functions\.(\w+):\d+\s*<\|tool_call_argument_begin\|>\s*(\{[^}]+\})\s*<\|tool_call_end\|>'
|
| 466 |
-
|
| 467 |
-
matches = re.findall(pipe_pattern, text, re.DOTALL)
|
| 468 |
-
|
| 469 |
-
if matches:
|
| 470 |
-
print(f"Found {len(matches)} tool call(s) via PIPE pattern")
|
| 471 |
-
for tool_name, args_json in matches:
|
| 472 |
-
try:
|
| 473 |
-
args = json.loads(args_json)
|
| 474 |
-
tool_calls.append((tool_name, args))
|
| 475 |
-
print(f"Parsed: {tool_name}({args})")
|
| 476 |
-
except json.JSONDecodeError as e:
|
| 477 |
-
print(f"JSON parse failed for {tool_name}: {args_json[:100]} - {e}")
|
| 478 |
-
|
| 479 |
-
# -------------------------------------------------------------------------
|
| 480 |
-
# PATTERN 2: Pipe pattern without closing tag (sometimes Kimi truncates)
|
| 481 |
-
# -------------------------------------------------------------------------
|
| 482 |
-
if not tool_calls:
|
| 483 |
-
pipe_partial = r'<\|tool_call_begin\|>\s*functions\.(\w+):\d+\s*<\|tool_call_argument_begin\|>\s*(\{[^}]+\})'
|
| 484 |
-
matches = re.findall(pipe_partial, text, re.DOTALL)
|
| 485 |
-
|
| 486 |
-
if matches:
|
| 487 |
-
print(f"Found {len(matches)} tool call(s) via PIPE pattern (no end tag)")
|
| 488 |
-
for tool_name, args_json in matches:
|
| 489 |
-
try:
|
| 490 |
-
args = json.loads(args_json)
|
| 491 |
-
tool_calls.append((tool_name, args))
|
| 492 |
-
print(f"Parsed (partial): {tool_name}({args})")
|
| 493 |
-
except json.JSONDecodeError as e:
|
| 494 |
-
print(f"JSON parse failed: {args_json[:100]} - {e}")
|
| 495 |
-
|
| 496 |
-
# -------------------------------------------------------------------------
|
| 497 |
-
# PATTERN 3: XML-style tags (fallback, less common)
|
| 498 |
-
# -------------------------------------------------------------------------
|
| 499 |
-
if not tool_calls:
|
| 500 |
-
xml_pattern = r'<tool_call_begin>\s*functions\.(\w+):\d+\s*<tool_call_argument_begin>\s*(\{[^}]+\})\s*<tool_call_end>'
|
| 501 |
-
matches = re.findall(xml_pattern, text, re.DOTALL)
|
| 502 |
-
|
| 503 |
-
if matches:
|
| 504 |
-
print(f"Found {len(matches)} tool call(s) via XML pattern")
|
| 505 |
-
for tool_name, args_json in matches:
|
| 506 |
-
try:
|
| 507 |
-
args = json.loads(args_json)
|
| 508 |
-
tool_calls.append((tool_name, args))
|
| 509 |
-
print(f"Parsed (XML): {tool_name}({args})")
|
| 510 |
-
except json.JSONDecodeError as e:
|
| 511 |
-
print(f"JSON parse failed: {args_json[:100]} - {e}")
|
| 512 |
-
|
| 513 |
-
# -------------------------------------------------------------------------
|
| 514 |
-
# PATTERN 4: XML without closing tag
|
| 515 |
-
# -------------------------------------------------------------------------
|
| 516 |
-
if not tool_calls:
|
| 517 |
-
xml_partial = r'<tool_call_begin>\s*functions\.(\w+):\d+\s*<tool_call_argument_begin>\s*(\{[^}]+\})'
|
| 518 |
-
matches = re.findall(xml_partial, text, re.DOTALL)
|
| 519 |
-
|
| 520 |
-
if matches:
|
| 521 |
-
print(f"Found {len(matches)} tool call(s) via XML pattern (no end tag)")
|
| 522 |
-
for tool_name, args_json in matches:
|
| 523 |
-
try:
|
| 524 |
-
args = json.loads(args_json)
|
| 525 |
-
tool_calls.append((tool_name, args))
|
| 526 |
-
print(f"Parsed (XML partial): {tool_name}({args})")
|
| 527 |
-
except json.JSONDecodeError as e:
|
| 528 |
-
print(f"JSON parse failed: {args_json[:100]} - {e}")
|
| 529 |
-
|
| 530 |
-
# -------------------------------------------------------------------------
|
| 531 |
-
# DEBUG: If we see tool-related text but couldn't parse anything
|
| 532 |
-
# -------------------------------------------------------------------------
|
| 533 |
-
if not tool_calls:
|
| 534 |
-
# Check for various indicators that a tool call might be present
|
| 535 |
-
indicators = [
|
| 536 |
-
'<|tool_call',
|
| 537 |
-
'<tool_call',
|
| 538 |
-
'functions.',
|
| 539 |
-
'tool_calls_section'
|
| 540 |
-
]
|
| 541 |
-
|
| 542 |
-
for indicator in indicators:
|
| 543 |
-
if indicator in text:
|
| 544 |
-
print(f"Tool indicator '{indicator}' found but parsing failed!")
|
| 545 |
-
# Show relevant snippet for debugging
|
| 546 |
-
idx = text.find(indicator)
|
| 547 |
-
snippet = text[max(0, idx-20):min(len(text), idx+200)]
|
| 548 |
-
print(f" Snippet: ...{snippet}...")
|
| 549 |
-
break
|
| 550 |
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
"""
|
| 556 |
-
Convert keyword queries into semantic queries for better VDB results.
|
| 557 |
-
|
| 558 |
-
RATIONALE:
|
| 559 |
-
Kimi tends to use short keywords ("Kid Rock", "*") which work poorly
|
| 560 |
-
for semantic search. We expand these into conceptual queries.
|
| 561 |
-
|
| 562 |
-
Examples:
|
| 563 |
-
- "Kid Rock" -> "discussions about Kid Rock or music and celebrities"
|
| 564 |
-
- "*" -> "recent conversation topics and context"
|
| 565 |
-
- "previous conversation" -> "topics we've discussed before"
|
| 566 |
-
"""
|
| 567 |
-
query = query.strip()
|
| 568 |
-
|
| 569 |
-
# Wildcard or empty - get recent context
|
| 570 |
-
if query in ["*", "", "all"]:
|
| 571 |
-
return "recent conversation topics and context"
|
| 572 |
-
|
| 573 |
-
# Very short (single word or name) - expand conceptually
|
| 574 |
-
if len(query.split()) <= 2:
|
| 575 |
-
return f"discussions about {query} or related topics"
|
| 576 |
-
|
| 577 |
-
# Already decent query - slight enhancement
|
| 578 |
-
if len(query) < 20:
|
| 579 |
-
return f"conversations related to {query}"
|
| 580 |
-
|
| 581 |
-
# Long query - assume it's already semantic
|
| 582 |
-
return query
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
def execute_tool(tool_name, args, ctx):
|
| 586 |
-
"""
|
| 587 |
-
Execute a tool and return results.
|
| 588 |
-
|
| 589 |
-
CHANGELOG [2025-01-30 - Josh]
|
| 590 |
-
Maps Kimi's tool names to actual RecursiveContextManager methods.
|
| 591 |
-
Enhances queries for semantic search tools.
|
| 592 |
-
"""
|
| 593 |
-
# Enhance queries for search tools
|
| 594 |
-
if "search" in tool_name and "query" in args:
|
| 595 |
-
original_query = args["query"]
|
| 596 |
-
args["query"] = enhance_query_for_semantic_search(original_query)
|
| 597 |
-
print(f"Enhanced query: '{original_query}' -> '{args['query']}'")
|
| 598 |
-
|
| 599 |
-
# Map tool names to actual methods
|
| 600 |
-
tool_map = {
|
| 601 |
-
"search_conversations": ctx.search_conversations,
|
| 602 |
-
"search_code": ctx.search_code,
|
| 603 |
-
"read_file": ctx.read_file,
|
| 604 |
-
"list_files": ctx.list_files,
|
| 605 |
-
"search_testament": ctx.search_testament,
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
if tool_name not in tool_map:
|
| 609 |
-
return f"Error: Unknown tool '{tool_name}'"
|
| 610 |
-
|
| 611 |
-
try:
|
| 612 |
-
print(f"Executing: {tool_name}({args})")
|
| 613 |
-
result = tool_map[tool_name](**args)
|
| 614 |
-
print(f"Tool returned: {str(result)[:200]}...")
|
| 615 |
-
return result
|
| 616 |
-
except Exception as e:
|
| 617 |
-
error_msg = f"Error executing {tool_name}: {e}"
|
| 618 |
-
print(f"{error_msg}")
|
| 619 |
-
return error_msg
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
def get_recent_context(history, n=5):
|
| 623 |
-
"""
|
| 624 |
-
Get last N conversation turns for auto-context injection.
|
| 625 |
-
|
| 626 |
-
Gradio 6.0+ format: [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
|
| 627 |
-
"""
|
| 628 |
-
if not history or len(history) < 2:
|
| 629 |
-
return ""
|
| 630 |
-
|
| 631 |
-
# Get last N*2 messages (each turn = user + assistant)
|
| 632 |
-
recent = history[-(n*2):]
|
| 633 |
-
|
| 634 |
-
context_parts = []
|
| 635 |
-
for msg in recent:
|
| 636 |
-
role = msg.get("role", "unknown")
|
| 637 |
-
content = msg.get("content", "")
|
| 638 |
-
context_parts.append(f"{role}: {content[:200]}...")
|
| 639 |
-
|
| 640 |
-
return "Recent context:\n" + "\n".join(context_parts)
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
# Create Gradio interface
|
| 644 |
-
with gr.Blocks(title="Clawdbot - E-T Systems Dev Assistant") as demo:
|
| 645 |
-
|
| 646 |
-
gr.Markdown("""
|
| 647 |
-
# Clawdbot: E-T Systems Development Assistant
|
| 648 |
-
|
| 649 |
-
*Powered by Kimi K2.5 Agent Swarm - Recursive Context - Persistent Memory*
|
| 650 |
-
|
| 651 |
-
Ask about code, upload files (images/PDFs/videos), or discuss architecture.
|
| 652 |
-
I have full codebase access through semantic search and persistent conversation memory.
|
| 653 |
-
""")
|
| 654 |
-
|
| 655 |
-
with gr.Row():
|
| 656 |
-
with gr.Column(scale=3):
|
| 657 |
-
chatbot = gr.Chatbot(
|
| 658 |
-
height=600,
|
| 659 |
-
show_label=False
|
| 660 |
-
)
|
| 661 |
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
label="Message",
|
| 666 |
-
lines=2,
|
| 667 |
-
scale=4
|
| 668 |
-
)
|
| 669 |
-
upload = gr.File(
|
| 670 |
-
label="Upload",
|
| 671 |
-
file_types=["image", ".pdf", ".mp4", ".mov", ".txt", ".md", ".py"],
|
| 672 |
-
type="filepath",
|
| 673 |
-
scale=1
|
| 674 |
-
)
|
| 675 |
|
| 676 |
with gr.Row():
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
gr.Markdown("
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
# Pull storage diagnostics from the context manager itself
|
| 701 |
-
# rather than guessing from env vars
|
| 702 |
-
stats_dict = ctx.get_stats() if hasattr(ctx, 'get_stats') else {}
|
| 703 |
-
storage_path = stats_dict.get("storage_path", "unknown")
|
| 704 |
-
cloud_configured = stats_dict.get("cloud_backup_configured", False)
|
| 705 |
-
cloud_repo = stats_dict.get("cloud_backup_repo", "Not set")
|
| 706 |
-
|
| 707 |
-
# Determine storage status with clear visual indicators
|
| 708 |
-
if "/data/" in storage_path:
|
| 709 |
-
storage_status = f"Storage: {storage_path} (PERSISTENT)"
|
| 710 |
-
else:
|
| 711 |
-
storage_status = f"Storage: {storage_path} (EPHEMERAL - enable persistent storage in Settings!)"
|
| 712 |
-
|
| 713 |
-
if cloud_configured:
|
| 714 |
-
cloud_status = f"Cloud Backup: {cloud_repo}"
|
| 715 |
-
else:
|
| 716 |
-
cloud_status = "Cloud Backup: NOT SET - Add MEMORY_REPO to Space secrets"
|
| 717 |
-
|
| 718 |
-
return f"""
|
| 719 |
-
**Repository:** {ctx.repo_path}
|
| 720 |
-
|
| 721 |
-
**Files Indexed:** {ctx.collection.count() if hasattr(ctx, 'collection') else 'Initializing...'}
|
| 722 |
-
|
| 723 |
-
**Conversations Saved:** {conv_count}
|
| 724 |
-
|
| 725 |
-
**{storage_status}**
|
| 726 |
-
|
| 727 |
-
**{cloud_status}**
|
| 728 |
-
|
| 729 |
-
**Model:** Kimi K2.5 Agent Swarm
|
| 730 |
-
|
| 731 |
-
**Context Mode:** Recursive Retrieval
|
| 732 |
-
|
| 733 |
-
*Unlimited context - searches code AND past conversations!*
|
| 734 |
-
"""
|
| 735 |
-
|
| 736 |
-
stats = gr.Markdown(get_stats())
|
| 737 |
-
refresh_stats = gr.Button("Refresh Stats")
|
| 738 |
-
|
| 739 |
-
# CHANGELOG [2025-01-30 - Claude]
|
| 740 |
-
# Added manual backup button for peace of mind
|
| 741 |
-
def force_backup():
|
| 742 |
-
ctx = initialize_context()
|
| 743 |
-
if hasattr(ctx, 'force_backup'):
|
| 744 |
-
ctx.force_backup()
|
| 745 |
-
return "Backup complete!"
|
| 746 |
-
return "Backup not available"
|
| 747 |
-
|
| 748 |
-
backup_btn = gr.Button("Backup Now")
|
| 749 |
-
backup_status = gr.Markdown("")
|
| 750 |
-
|
| 751 |
-
gr.Markdown("### Example Queries")
|
| 752 |
-
gr.Markdown("""
|
| 753 |
-
- "How does Genesis handle surprise detection?"
|
| 754 |
-
- "Show me the Observatory API implementation"
|
| 755 |
-
- "Add email notifications to Cricket"
|
| 756 |
-
- "Review this code for architectural consistency"
|
| 757 |
-
- "What Testament decisions relate to vector storage?"
|
| 758 |
-
""")
|
| 759 |
-
|
| 760 |
-
gr.Markdown("### Available Tools")
|
| 761 |
-
gr.Markdown("""
|
| 762 |
-
- `search_code()` - Semantic search
|
| 763 |
-
- `read_file()` - Read specific files
|
| 764 |
-
- `search_testament()` - Query decisions
|
| 765 |
-
- `list_files()` - Browse structure
|
| 766 |
-
- `search_conversations()` - Memory recall
|
| 767 |
-
""")
|
| 768 |
-
|
| 769 |
-
# Event handlers - Gradio 6.0 message format with MULTIMODAL support
|
| 770 |
-
def handle_submit(message, uploaded_file, history):
|
| 771 |
-
"""
|
| 772 |
-
Handle message submission with multimodal support and translation layer.
|
| 773 |
-
|
| 774 |
-
CHANGELOG [2025-01-30 - Josh]
|
| 775 |
-
Phase 1: Translation layer for Kimi's tool calling
|
| 776 |
-
Phase 2: Multimodal file upload (images, PDFs, videos)
|
| 777 |
-
|
| 778 |
-
CHANGELOG [2025-01-30 - Claude]
|
| 779 |
-
Added cloud backup integration via RecursiveContextManager.
|
| 780 |
-
|
| 781 |
-
CHANGELOG [2025-01-31 - Claude]
|
| 782 |
-
Added tool execution loop - keeps calling model until no more tool calls.
|
| 783 |
-
Previously: Single tool call -> single followup -> done (broken if multi-tool)
|
| 784 |
-
Now: Loop until response has no tool calls (proper agentic behavior)
|
| 785 |
-
|
| 786 |
-
Kimi K2.5 is natively multimodal, so we can send:
|
| 787 |
-
- Images -> Vision analysis
|
| 788 |
-
- PDFs -> Document understanding
|
| 789 |
-
- Videos -> Content analysis
|
| 790 |
-
- Code files -> Review and integration
|
| 791 |
-
|
| 792 |
-
The translation layer:
|
| 793 |
-
1. Parses Kimi's native tool call format
|
| 794 |
-
2. Enhances queries for semantic search
|
| 795 |
-
3. Executes tools via RecursiveContextManager
|
| 796 |
-
4. Injects results + recent context back to Kimi
|
| 797 |
-
5. Loops until no more tool calls
|
| 798 |
-
6. Saves all conversations to ChromaDB AND cloud for persistence
|
| 799 |
-
"""
|
| 800 |
-
if not message.strip() and not uploaded_file:
|
| 801 |
-
return history, "", None # Clear file upload too
|
| 802 |
-
|
| 803 |
-
ctx = initialize_context()
|
| 804 |
-
|
| 805 |
-
# Process uploaded file if present
|
| 806 |
-
file_context = ""
|
| 807 |
-
if uploaded_file:
|
| 808 |
-
file_path = uploaded_file
|
| 809 |
-
file_name = os.path.basename(file_path)
|
| 810 |
-
file_ext = os.path.splitext(file_name)[1].lower()
|
| 811 |
-
|
| 812 |
-
print(f"Processing uploaded file: {file_name}")
|
| 813 |
-
|
| 814 |
-
# Handle different file types
|
| 815 |
-
if file_ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
|
| 816 |
-
# Image - Kimi will analyze via vision
|
| 817 |
-
file_context = f"\n\n[User uploaded image: {file_name}]"
|
| 818 |
-
# TODO: Add image to message content for Kimi's vision
|
| 819 |
-
|
| 820 |
-
elif file_ext == '.pdf':
|
| 821 |
-
# PDF - can extract text or let Kimi process
|
| 822 |
-
file_context = f"\n\n[User uploaded PDF: {file_name}]"
|
| 823 |
-
# TODO: Extract PDF text or send to Kimi
|
| 824 |
-
|
| 825 |
-
elif file_ext in ['.mp4', '.mov', '.avi']:
|
| 826 |
-
# Video - describe for Kimi
|
| 827 |
-
file_context = f"\n\n[User uploaded video: {file_name}]"
|
| 828 |
-
# TODO: Video frame extraction or description
|
| 829 |
-
|
| 830 |
-
elif file_ext in ['.txt', '.md', '.py', '.js', '.ts']:
|
| 831 |
-
# Text files - read and include
|
| 832 |
-
try:
|
| 833 |
-
with open(file_path, 'r') as f:
|
| 834 |
-
content = f.read()
|
| 835 |
-
file_context = f"\n\n[User uploaded {file_name}]:\n```{file_ext[1:]}\n{content}\n```"
|
| 836 |
-
except Exception as e:
|
| 837 |
-
file_context = f"\n\n[Error reading {file_name}: {e}]"
|
| 838 |
-
|
| 839 |
-
# Combine message with file context
|
| 840 |
-
full_message = message + file_context if file_context else message
|
| 841 |
-
|
| 842 |
-
# =====================================================================
|
| 843 |
-
# TOOL EXECUTION LOOP
|
| 844 |
-
# =====================================================================
|
| 845 |
-
# Keep calling model and executing tools until we get a clean response
|
| 846 |
-
# Max iterations prevents infinite loops
|
| 847 |
-
# =====================================================================
|
| 848 |
-
|
| 849 |
-
MAX_TOOL_ITERATIONS = 5
|
| 850 |
-
current_history = history.copy()
|
| 851 |
-
response = ""
|
| 852 |
-
tool_injection_message = ""
|
| 853 |
-
|
| 854 |
-
for iteration in range(MAX_TOOL_ITERATIONS):
|
| 855 |
-
print(f"\nTool loop iteration {iteration + 1}/{MAX_TOOL_ITERATIONS}")
|
| 856 |
-
|
| 857 |
-
# Get response from Kimi
|
| 858 |
-
if iteration == 0:
|
| 859 |
-
# First call - use the original message
|
| 860 |
-
response = chat(full_message, current_history)
|
| 861 |
-
else:
|
| 862 |
-
# Subsequent calls - inject tool results
|
| 863 |
-
response = chat(tool_injection_message, current_history)
|
| 864 |
-
|
| 865 |
-
# Check for tool calls
|
| 866 |
-
tool_calls = parse_kimi_tool_call(response)
|
| 867 |
-
|
| 868 |
-
if not tool_calls:
|
| 869 |
-
# No tool calls - we're done!
|
| 870 |
-
print(f"No tool calls detected, final response ready")
|
| 871 |
-
break
|
| 872 |
-
|
| 873 |
-
print(f"Found {len(tool_calls)} tool call(s), executing...")
|
| 874 |
-
|
| 875 |
-
# Execute all tool calls
|
| 876 |
-
tool_results = []
|
| 877 |
-
for tool_name, args in tool_calls:
|
| 878 |
-
result = execute_tool(tool_name, args, ctx)
|
| 879 |
-
tool_results.append({
|
| 880 |
-
"tool": tool_name,
|
| 881 |
-
"args": args,
|
| 882 |
-
"result": result
|
| 883 |
-
})
|
| 884 |
-
|
| 885 |
-
# Build injection message with results
|
| 886 |
-
results_text = "\n\n".join([
|
| 887 |
-
f"**{r['tool']}({r['args']}):**\n{r['result']}"
|
| 888 |
-
for r in tool_results
|
| 889 |
-
])
|
| 890 |
-
|
| 891 |
-
recent_context = get_recent_context(current_history, n=3)
|
| 892 |
-
|
| 893 |
-
tool_injection_message = f"""Tool execution results:
|
| 894 |
-
|
| 895 |
-
{results_text}
|
| 896 |
-
|
| 897 |
-
{recent_context}
|
| 898 |
-
|
| 899 |
-
Based on these tool results, please provide your response to the user's original question. If you need more information, you can call additional tools."""
|
| 900 |
-
|
| 901 |
-
# Add the exchange to history for context
|
| 902 |
-
if iteration == 0:
|
| 903 |
-
current_history.append({"role": "user", "content": full_message})
|
| 904 |
-
current_history.append({"role": "assistant", "content": f"[Tool calls: {', '.join(t[0] for t in tool_calls)}]"})
|
| 905 |
-
current_history.append({"role": "user", "content": tool_injection_message})
|
| 906 |
-
|
| 907 |
-
else:
|
| 908 |
-
# Hit max iterations - append warning
|
| 909 |
-
print(f"Hit max tool iterations ({MAX_TOOL_ITERATIONS})")
|
| 910 |
-
response += "\n\n*[Note: Reached maximum tool call depth]*"
|
| 911 |
-
|
| 912 |
-
# =====================================================================
|
| 913 |
-
# FINALIZE RESPONSE
|
| 914 |
-
# =====================================================================
|
| 915 |
-
|
| 916 |
-
# Gradio 6.0+ format: list of dicts with 'role' and 'content'
|
| 917 |
-
history.append({"role": "user", "content": full_message})
|
| 918 |
-
history.append({"role": "assistant", "content": response})
|
| 919 |
-
|
| 920 |
-
# PERSISTENCE: Save this conversation turn (now with cloud backup!)
|
| 921 |
-
turn_id = len(history) // 2
|
| 922 |
-
try:
|
| 923 |
-
ctx.save_conversation_turn(full_message, response, turn_id)
|
| 924 |
-
except Exception as e:
|
| 925 |
-
print(f"Failed to save conversation: {e}")
|
| 926 |
-
|
| 927 |
-
return history, "", None # Clear textbox AND file upload
|
| 928 |
-
|
| 929 |
-
submit.click(handle_submit, [msg, upload, chatbot], [chatbot, msg, upload])
|
| 930 |
-
msg.submit(handle_submit, [msg, upload, chatbot], [chatbot, msg, upload])
|
| 931 |
-
clear.click(lambda: ([], "", None), None, [chatbot, msg, upload], queue=False)
|
| 932 |
-
refresh_stats.click(get_stats, None, stats)
|
| 933 |
-
backup_btn.click(force_backup, None, backup_status)
|
| 934 |
-
|
| 935 |
|
| 936 |
-
# Launch when run directly
|
| 937 |
if __name__ == "__main__":
|
| 938 |
-
|
| 939 |
-
initialize_context()
|
| 940 |
-
print("Context manager ready")
|
| 941 |
-
print("Launching Gradio interface...")
|
| 942 |
-
demo.launch(
|
| 943 |
-
server_name="0.0.0.0",
|
| 944 |
-
server_port=7860,
|
| 945 |
-
show_error=True
|
| 946 |
-
)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Clawdbot Phase 1 Orchestrator
|
| 3 |
+
[CHANGELOG 2026-01-31 - Gemini]
|
| 4 |
+
ADDED: HITL Gate for Windows-style "Step-through" approvals.
|
| 5 |
+
ADDED: Shadow Branch Failsafe logic via RecursiveContextManager.
|
| 6 |
+
ADDED: Vector-Native substrate mandate for E-T Systems.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
from huggingface_hub import InferenceClient
|
| 11 |
+
from recursive_context import RecursiveContextManager
|
| 12 |
+
import os, re, json
|
| 13 |
|
| 14 |
+
# --- STATE MANAGEMENT ---
|
| 15 |
+
repo_path = os.getenv("REPO_PATH", "/workspace/e-t-systems")
|
| 16 |
+
ctx = RecursiveContextManager(repo_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
class ProposalManager:
|
| 19 |
+
def __init__(self):
|
| 20 |
+
self.pending = []
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
def add(self, tool, args):
|
| 23 |
+
# Format for CheckboxGroup
|
| 24 |
+
label = f"{tool}: {args.get('path', args.get('command', 'unknown'))}"
|
| 25 |
+
self.pending.append({"label": label, "tool": tool, "args": args})
|
| 26 |
+
return label
|
|
|
|
| 27 |
|
| 28 |
+
def get_labels(self):
|
| 29 |
+
return [p["label"] for p in self.pending]
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
proposals = ProposalManager()
|
| 32 |
|
| 33 |
+
def execute_tool_orchestrated(tool_name, args):
|
| 34 |
+
"""Orchestrates tool execution with HITL interrupts."""
|
| 35 |
+
if tool_name in ["write_file", "shell_execute"]:
|
| 36 |
+
# First write in a session triggers shadow branch
|
| 37 |
+
if not proposals.pending:
|
| 38 |
+
ctx.create_shadow_branch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
label = proposals.add(tool_name, args)
|
| 41 |
+
return f"⏳ PROPOSAL STAGED: {label}. Please review in the 'Build Approval' tab."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
# Immediate execution for read-only tools
|
| 44 |
+
mapping = {"search_code": ctx.search_code, "read_file": ctx.read_file}
|
| 45 |
+
return mapping[tool_name](**args) if tool_name in mapping else "Unknown tool."
|
| 46 |
|
| 47 |
+
# --- UI COMPONENTS ---
|
| 48 |
+
with gr.Blocks(title="Clawdbot Orchestrator") as demo:
|
| 49 |
+
gr.Markdown("# 🦞 Clawdbot: E-T Systems Orchestrator")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
with gr.Tabs() as tabs:
|
| 52 |
+
with gr.Tab("Vibe Chat", id="chat_tab"):
|
| 53 |
+
chatbot = gr.Chatbot(type="messages")
|
| 54 |
+
msg = gr.Textbox(placeholder="Describe the build task...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
with gr.Tab("Build Approval Gate", id="build_tab"):
|
| 57 |
+
gr.Markdown("### 🛠️ Pending Build Proposals")
|
| 58 |
+
gate_list = gr.CheckboxGroup(label="Select actions to execute", choices=[])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
with gr.Row():
|
| 61 |
+
btn_exec = gr.Button("✅ Execute Selected", variant="primary")
|
| 62 |
+
btn_all = gr.Button("🚀 Accept All & Build")
|
| 63 |
+
btn_clear = gr.Button("❌ Reject All", variant="stop")
|
| 64 |
+
|
| 65 |
+
status_out = gr.Markdown("No pending builds.")
|
| 66 |
+
|
| 67 |
+
# --- UI LOGIC ---
|
| 68 |
+
def process_selected(selected):
|
| 69 |
+
results = []
|
| 70 |
+
for label in selected:
|
| 71 |
+
for p in proposals.pending:
|
| 72 |
+
if p["label"] == label:
|
| 73 |
+
res = execute_tool_direct(p["tool"], p["args"])
|
| 74 |
+
results.append(res)
|
| 75 |
+
proposals.pending = [p for p in proposals.pending if p["label"] not in selected]
|
| 76 |
+
return gr.update(choices=proposals.get_labels()), f"Executed: {len(results)} actions."
|
| 77 |
+
|
| 78 |
+
def execute_tool_direct(name, args):
|
| 79 |
+
if name == "write_file": return ctx.write_file(**args)
|
| 80 |
+
if name == "shell_execute": return ctx.shell_execute(**args)
|
| 81 |
+
|
| 82 |
+
btn_exec.click(process_selected, inputs=[gate_list], outputs=[gate_list, status_out])
|
| 83 |
+
# Additional event logic would be linked here for 'Accept All' and 'Reject'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
|
|
|
| 85 |
if __name__ == "__main__":
|
| 86 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|