Executor-Tyrant-Framework commited on
Commit
8682d5a
·
verified ·
1 Parent(s): ce41723

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -926
app.py CHANGED
@@ -1,946 +1,86 @@
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
- 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
- # HuggingFace client will be initialized in chat function
54
- # (Spaces sets HF_TOKEN as environment variable)
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
- CODEBASE ACCESS:
359
- The E-T Systems codebase is loaded and indexed at /workspace/e-t-systems/
360
- - Use search_code() for semantic search across files
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
- Your capabilities:
366
- - Agent swarm (spawn up to 100 sub-agents for complex tasks)
367
- - Native multimodal (vision + code)
368
- - 256K context window
369
- - Direct codebase access via tools
370
- - Persistent memory across sessions (CLOUD BACKED!)
371
 
372
- When helping with code:
373
- 1. USE TOOLS to understand existing code first
374
- 2. Search past conversations for context
375
- 3. Generate code that fits the architecture
376
- 4. Explain your reasoning clearly
377
 
378
- You are Kimi K2.5 running as Clawdbot with automatic tool translation and persistent memory."""
379
 
380
- messages = [{"role": "system", "content": system_content}]
381
-
382
- # Add history (Gradio 6.0+ dict format works directly with API)
383
- messages.extend(history)
384
-
385
- # Add current message
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
- # Provide helpful error messages
407
- if "Rate limit" in error_msg or "429" in error_msg:
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
- # TRANSLATION LAYER: Parse Kimi's native tool calling format
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
- return tool_calls
552
-
553
-
554
- def enhance_query_for_semantic_search(query):
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
- with gr.Row():
663
- msg = gr.Textbox(
664
- placeholder="Ask about code, or upload files for analysis...",
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
- submit = gr.Button("Send", variant="primary")
678
- clear = gr.Button("Clear")
679
-
680
- with gr.Column(scale=1):
681
- gr.Markdown("### Context Info")
682
-
683
- def get_stats():
684
- """
685
- Get current stats including storage and cloud backup diagnostics.
686
-
687
- CHANGELOG [2025-01-30 - Claude]
688
- Added cloud backup status indicator.
689
-
690
- CHANGELOG [2025-01-31 - Claude]
691
- Added storage path display and persistent vs ephemeral indicator.
692
- Now shows exactly where ChromaDB lives and whether it will survive
693
- a restart, so Josh can diagnose persistence issues at a glance.
694
- """
695
- ctx = initialize_context()
696
- conv_count = ctx.get_conversation_count() if hasattr(ctx, 'get_conversation_count') else 0
697
-
698
- # Get detailed stats from context manager
699
- # CHANGELOG [2025-01-31 - Claude]
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
- print("Initializing Clawdbot...")
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)